diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index eaa17c36e..248b47bb5 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -188,7 +188,7 @@ runs: runtime: ${{ inputs.runtime }} - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: build-${{ inputs.runtime }} path: _artifacts/**/* diff --git a/.github/actions/package/action.yml b/.github/actions/package/action.yml index 2031a45c6..a3d1905ac 100644 --- a/.github/actions/package/action.yml +++ b/.github/actions/package/action.yml @@ -25,13 +25,13 @@ runs: using: "composite" steps: - name: Download Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: ${{ inputs.artifact }} path: _output - name: Download UI Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: build_ui path: _output/UI @@ -67,7 +67,7 @@ runs: build.bat - name: Upload Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: release-${{ inputs.runtime }} compression-level: 0 diff --git a/.github/actions/publish-test-artifact/action.yml b/.github/actions/publish-test-artifact/action.yml index af3642043..6d5fc7ba2 100644 --- a/.github/actions/publish-test-artifact/action.yml +++ b/.github/actions/publish-test-artifact/action.yml @@ -12,7 +12,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: tests-${{ inputs.runtime }} path: _tests/${{ inputs.framework }}/${{ inputs.runtime }}/publish/**/* diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index a516f0b1b..2ef8a862f 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -85,7 +85,7 @@ runs: - name: Upload Test Results if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: results-${{ env.RESULTS_NAME }} path: TestResults/*.trx diff --git a/.github/workflows/api_docs.yml b/.github/workflows/api_docs.yml index e657cf62e..27f8827a8 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@v4 + - uses: actions/checkout@v6 - name: Setup dotnet uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/build_v5.yml b/.github/workflows/build_v5.yml index 13989248a..0d52a4c15 100644 --- a/.github/workflows/build_v5.yml +++ b/.github/workflows/build_v5.yml @@ -82,7 +82,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Build uses: ./.github/actions/build @@ -97,7 +97,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Volta uses: volta-cli/action@v4 @@ -115,7 +115,7 @@ jobs: run: yarn build --env production - name: Publish UI Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: build_ui path: _output/UI/**/* @@ -139,7 +139,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Test uses: ./.github/actions/test @@ -158,7 +158,7 @@ jobs: postgres-version: [16, 17, 18] steps: - name: Check out - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Test uses: ./.github/actions/test @@ -195,7 +195,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Test uses: ./.github/actions/test diff --git a/.github/workflows/close_invalid_issues.yml b/.github/workflows/close_invalid_issues.yml new file mode 100644 index 000000000..601a61c29 --- /dev/null +++ b/.github/workflows/close_invalid_issues.yml @@ -0,0 +1,26 @@ +name: Close issues without labels + +on: + issues: + types: + - opened + - reopened + +jobs: + close-issue: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + sparse-checkout: | + .github + - name: Close issue if no labels found + if: join(github.event.issue.labels) == '' + run: | + gh issue comment ${{ github.event.issue.number }} --body ":wave: @${{ github.event.issue.user.login }}, this issue was closed automatically because it was created without following an issue template. Please update the issue following the correct template for this issue. Once updated please reply to this issue so we can review and re-open. In the future, use the [issue templates](https://github.com/${{ github.repository }}/issues/new/choose) instead of creating your own." + gh issue close ${{ github.event.issue.number }} --reason "not planned" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/close_stale_draft_prs.yml b/.github/workflows/close_stale_draft_prs.yml new file mode 100644 index 000000000..f5704686d --- /dev/null +++ b/.github/workflows/close_stale_draft_prs.yml @@ -0,0 +1,26 @@ +name: Close stale draft PRs + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + close-stale-drafts: + runs-on: ubuntu-latest + if: github.repository == 'Sonarr/Sonarr' + permissions: + pull-requests: write + steps: + - name: Close draft PRs inactive for 90+ days + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + cutoff=$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ) + gh pr list --draft --state open --limit 1000 \ + --json number,updatedAt \ + --jq ".[] | select(.updatedAt < \"$cutoff\") | .number" \ + | while read -r pr; do + gh pr close "$pr" --comment ":wave: This draft pull request has been closed automatically because it has not had any activity in 90 days. If you are still working on this, please update it and let us know so we can reopen it." + done diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e560a406b..ac406242d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -52,7 +52,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Package uses: ./.github/actions/package @@ -71,10 +71,10 @@ jobs: contents: write steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download release artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: _artifacts pattern: release-* diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx index b9c3a303e..caa909b60 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.tsx +++ b/frontend/src/Activity/Blocklist/Blocklist.tsx @@ -109,9 +109,10 @@ function BlocklistContent() { const handleClearBlocklistConfirmed = useCallback(() => { executeCommand({ name: CommandNames.ClearBlocklist }, () => { goToPage(1); + refetch(); }); setIsConfirmClearModalOpen(false); - }, [setIsConfirmClearModalOpen, goToPage, executeCommand]); + }, [setIsConfirmClearModalOpen, executeCommand, goToPage, refetch]); const handleConfirmClearModalClose = useCallback(() => { setIsConfirmClearModalOpen(false); diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.tsx b/frontend/src/Activity/Blocklist/BlocklistRow.tsx index cb6a6ffb9..3595203ef 100644 --- a/frontend/src/Activity/Blocklist/BlocklistRow.tsx +++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx @@ -137,10 +137,15 @@ function BlocklistRow({ if (name === 'actions') { return ( - + - + ); } diff --git a/frontend/src/Activity/Queue/Details/QueueDetailsProvider.tsx b/frontend/src/Activity/Queue/Details/QueueDetailsProvider.tsx index 05d1b8235..75976b452 100644 --- a/frontend/src/Activity/Queue/Details/QueueDetailsProvider.tsx +++ b/frontend/src/Activity/Queue/Details/QueueDetailsProvider.tsx @@ -46,7 +46,7 @@ export function useQueueItemForEpisode(episodeId: number) { const queue = useContext(QueueDetailsContext); return useMemo(() => { - return queue?.find((item) => item.episodeIds.includes(episodeId)); + return queue?.find((item) => item.episodeIds?.includes(episodeId)); }, [episodeId, queue]); } @@ -89,15 +89,15 @@ export function useQueueDetailsForSeries( return acc; } - if (seasonNumber != null && item.seasonNumber !== seasonNumber) { + if ( + seasonNumber != null && + !item.seasonNumbers?.includes(seasonNumber) + ) { return acc; } - acc.count++; - - if (item.episodeHasFile) { - acc.episodesWithFiles++; - } + acc.count += item.episodeIds.length; + acc.episodesWithFiles += item.episodesWithFilesCount; return acc; }, diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx index 459ff196d..dd8259487 100644 --- a/frontend/src/Activity/Queue/Queue.tsx +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -365,7 +365,7 @@ function QueueContent() { selectedIds.every((id: number) => { const item = records.find((i) => i.id === id); - return !!(item && item.seriesId && item.episodeId); + return !!(item && item.seriesId && item.episodeIds.length); }) } isPending={ diff --git a/frontend/src/Activity/Queue/QueueRow.tsx b/frontend/src/Activity/Queue/QueueRow.tsx index 2cc69d690..7879b013a 100644 --- a/frontend/src/Activity/Queue/QueueRow.tsx +++ b/frontend/src/Activity/Queue/QueueRow.tsx @@ -29,6 +29,8 @@ import Queue, { QueueTrackedDownloadStatus, StatusMessage, } from 'typings/Queue'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import translate from 'Utilities/String/translate'; @@ -106,7 +108,7 @@ function QueueRow(props: QueueRowProps) { const series = useSingleSeries(seriesId); const episodes = useEpisodesWithIds(episodeIds); - const { showRelativeDates, shortDateFormat, timeFormat } = + const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = useUiSettingsValues(); const { removeQueueItem, isRemoving } = useRemoveQueueItem(id); const { grabQueueItem, isGrabbing, grabError } = useGrabQueueItem(id); @@ -248,17 +250,39 @@ function QueueRow(props: QueueRowProps) { return ( - - {' - '} - + + {getRelativeDate({ + date: episodes[0].airDateUtc, + shortDateFormat, + showRelativeDates, + timeFormat, + timeForToday: true, + })} + {' - '} + {getRelativeDate({ + date: episodes[episodes.length - 1].airDateUtc, + shortDateFormat, + showRelativeDates, + timeFormat, + timeForToday: true, + })} + ); } @@ -369,6 +393,7 @@ function QueueRow(props: QueueRowProps) { {showInteractiveImport ? ( ) : null} @@ -377,6 +402,7 @@ function QueueRow(props: QueueRowProps) { diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeries.tsx b/frontend/src/AddSeries/AddNewSeries/AddNewSeries.tsx index 64b57dc4e..581023798 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeries.tsx +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeries.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import Alert from 'Components/Alert'; import TextInput from 'Components/Form/TextInput'; import Icon from 'Components/Icon'; @@ -22,6 +22,7 @@ function AddNewSeries() { const { term: initialTerm = '' } = useQueryParams<{ term: string }>(); const hasSeries = useHasSeries(); const [term, setTerm] = useState(initialTerm); + const searchInputRef = useRef(null); const [isFetching, setIsFetching] = useState(false); const query = useDebounce(term, term ? 300 : 0); @@ -36,6 +37,7 @@ function AddNewSeries() { const handleClearSeriesLookupPress = useCallback(() => { setTerm(''); setIsFetching(false); + searchInputRef.current?.focus(); }, []); const { isFetching: isFetchingApi, error, data } = useLookupSeries(query); @@ -57,6 +59,7 @@ function AddNewSeries() { - +
{isSmallScreen ? null : ( @@ -100,15 +107,26 @@ function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) { /> ) : null} + {isExcluded ? ( + + ) : null} +
diff --git a/frontend/src/AddSeries/AddNewSeries/useAddSeries.ts b/frontend/src/AddSeries/AddNewSeries/useAddSeries.ts index 086c031d9..016421e1f 100644 --- a/frontend/src/AddSeries/AddNewSeries/useAddSeries.ts +++ b/frontend/src/AddSeries/AddNewSeries/useAddSeries.ts @@ -1,7 +1,9 @@ import { useQueryClient } from '@tanstack/react-query'; import AddSeries from 'AddSeries/AddSeries'; import { AddSeriesOptions } from 'AddSeries/addSeriesOptionsStore'; -import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import useApiMutation, { + addOrUpdateQueryClientItem, +} from 'Helpers/Hooks/useApiMutation'; import useApiQuery from 'Helpers/Hooks/useApiQuery'; import Series from 'Series/Series'; @@ -42,13 +44,9 @@ export const useAddSeries = () => { method: 'POST', mutationOptions: { onSuccess: (newSeries) => { - queryClient.setQueryData(['/series'], (oldSeries) => { - if (!oldSeries) { - return [newSeries]; - } - - return [...oldSeries, newSeries]; - }); + queryClient.setQueryData(['/series'], (oldSeries = []) => + addOrUpdateQueryClientItem(oldSeries, newSeries, 'id') + ); }, }, } diff --git a/frontend/src/AddSeries/AddSeries.ts b/frontend/src/AddSeries/AddSeries.ts index 984edc74a..5daec31e8 100644 --- a/frontend/src/AddSeries/AddSeries.ts +++ b/frontend/src/AddSeries/AddSeries.ts @@ -2,6 +2,7 @@ import Series from 'Series/Series'; interface AddSeries extends Series { folder: string; + isExcluded: boolean; } export default AddSeries; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 162f03de4..ece88ef93 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -4,22 +4,15 @@ import AppSectionState, { AppSectionListState, AppSectionSaveState, AppSectionSchemaState, - PagedAppSectionState, } from 'App/State/AppSectionState'; -import Language from 'Language/Language'; import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging'; import CustomFormat from 'typings/CustomFormat'; import CustomFormatSpecification from 'typings/CustomFormatSpecification'; import DelayProfile from 'typings/DelayProfile'; 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'; import DownloadClientOptions from 'typings/Settings/DownloadClientOptions'; -import General from 'typings/Settings/General'; -import IndexerOptions from 'typings/Settings/IndexerOptions'; type Presets = T & { presets: T[]; @@ -53,10 +46,6 @@ export interface DownloadClientOptionsAppState extends AppSectionItemState, AppSectionSaveState {} -export interface GeneralAppState - extends AppSectionItemState, - AppSectionSaveState {} - export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -65,18 +54,6 @@ export interface ImportListAppState isTestingAll: boolean; } -export interface IndexerOptionsAppState - extends AppSectionItemState, - AppSectionSaveState {} - -export interface IndexerAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState, - AppSectionSchemaState> { - isTestingAll: boolean; -} - export interface CustomFormatAppState extends AppSectionState, AppSectionDeleteState, @@ -92,17 +69,6 @@ export interface ImportListOptionsSettingsAppState extends AppSectionItemState, AppSectionSaveState {} -export interface ImportListExclusionsSettingsAppState - extends AppSectionState, - AppSectionSaveState, - PagedAppSectionState, - AppSectionDeleteState { - pendingChanges: Partial; -} - -export type IndexerFlagSettingsAppState = AppSectionState; -export type LanguageSettingsAppState = AppSectionState; - interface SettingsAppState { autoTaggings: AutoTaggingAppState; autoTaggingSpecifications: AutoTaggingSpecificationAppState; @@ -111,14 +77,8 @@ interface SettingsAppState { delayProfiles: DelayProfileAppState; downloadClients: DownloadClientAppState; downloadClientOptions: DownloadClientOptionsAppState; - general: GeneralAppState; - importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; - indexerFlags: IndexerFlagSettingsAppState; - indexerOptions: IndexerOptionsAppState; - indexers: IndexerAppState; - languages: LanguageSettingsAppState; } export default SettingsAppState; diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.tsx b/frontend/src/Calendar/Agenda/AgendaEvent.tsx index 705fea29a..d0ab3e7fb 100644 --- a/frontend/src/Calendar/Agenda/AgendaEvent.tsx +++ b/frontend/src/Calendar/Agenda/AgendaEvent.tsx @@ -154,10 +154,7 @@ function AgendaEvent(props: AgendaEventProps) { {queueItem ? ( - + ) : null} diff --git a/frontend/src/Calendar/CalendarMissingEpisodeSearchButton.tsx b/frontend/src/Calendar/CalendarMissingEpisodeSearchButton.tsx index 91aa6d666..b206daa61 100644 --- a/frontend/src/Calendar/CalendarMissingEpisodeSearchButton.tsx +++ b/frontend/src/Calendar/CalendarMissingEpisodeSearchButton.tsx @@ -39,9 +39,7 @@ const useMissingEpisodeIdsSelector = () => { moment(airDateUtc).isAfter(start) && moment(airDateUtc).isBefore(end) && isBefore(episode.airDateUtc) && - !queueDetails.some( - (details) => !!details.episode && details.episode.id === episode.id - ) + !queueDetails.some((details) => details.episodeIds?.includes(episode.id)) ) { acc.push(episode.id); } diff --git a/frontend/src/Calendar/useCalendar.ts b/frontend/src/Calendar/useCalendar.ts index fcaaf5dbf..496b86513 100644 --- a/frontend/src/Calendar/useCalendar.ts +++ b/frontend/src/Calendar/useCalendar.ts @@ -118,7 +118,7 @@ const useCalendar = () => { return acc; }, { - includeUnmonitored: false, + includeUnmonitored: true, includeSpecials: true, } ); diff --git a/frontend/src/Commands/CommandNames.ts b/frontend/src/Commands/CommandNames.ts index 8cee7d38c..301a72d1f 100644 --- a/frontend/src/Commands/CommandNames.ts +++ b/frontend/src/Commands/CommandNames.ts @@ -5,6 +5,7 @@ enum CommandNames { ClearLog = 'ClearLog', CutoffUnmetEpisodeSearch = 'CutoffUnmetEpisodeSearch', DeleteLogFiles = 'DeleteLogFiles', + DeleteSeriesFiles = 'DeleteSeriesFiles', DeleteUpdateLogFiles = 'DeleteUpdateLogFiles', DownloadedEpisodesScan = 'DownloadedEpisodesScan', EpisodeSearch = 'EpisodeSearch', diff --git a/frontend/src/Commands/useCommands.ts b/frontend/src/Commands/useCommands.ts index f40579cb8..f4eed0752 100644 --- a/frontend/src/Commands/useCommands.ts +++ b/frontend/src/Commands/useCommands.ts @@ -2,7 +2,9 @@ import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo, useRef } from 'react'; import { showMessage } from 'App/messagesStore'; import Command, { CommandBody, NewCommandBody } from 'Commands/Command'; -import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import useApiMutation, { + addOrUpdateQueryClientItem, +} from 'Helpers/Hooks/useApiMutation'; import useApiQuery from 'Helpers/Hooks/useApiQuery'; import { ERROR, @@ -45,11 +47,8 @@ export const useExecuteCommand = () => { path: '/command', mutationOptions: { onSuccess: (newCommand: Command) => { - queryClient.setQueryData( - ['/command'], - (oldCommands = []) => { - return [...oldCommands, newCommand]; - } + queryClient.setQueryData(['/command'], (oldCommands = []) => + addOrUpdateQueryClientItem(oldCommands, newCommand, 'id') ); }, }, diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx index aaaf9b4e0..1f02f0145 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.tsx @@ -133,7 +133,11 @@ function FileBrowserModalContent({ className={styles.scroller} scrollDirection="both" > - {error ?
{translate('ErrorLoadingContents')}
: null} + {error ? ( + + {translate('ErrorLoadingContents')} + + ) : null} {isFetched && !error ? ( diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx index 0ccc3070d..0f437a642 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx @@ -10,6 +10,7 @@ import { import { DateFilterValue, FilterType } from 'Helpers/Props/filterTypes'; import { InputChanged } from 'typings/inputs'; import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import DefaultFilterBuilderRowValue from './DefaultFilterBuilderRowValue'; @@ -21,6 +22,7 @@ import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValue from './QualityFilterBuilderRowValue'; import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue'; +import ReleaseTypeFilterBuilderRowValue from './ReleaseTypeFilterBuilderRowValue'; import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue'; @@ -112,6 +114,9 @@ function getRowValueConnector( case filterBuilderValueTypes.MONITORED_STATUS: return MonitoredStatusFilterBuilderRowValue; + case filterBuilderValueTypes.RELEASE_TYPES: + return ReleaseTypeFilterBuilderRowValue; + case filterBuilderValueTypes.SERIES: return SeriesFilterBuilderRowValue; @@ -300,11 +305,16 @@ function FilterBuilderRow({
- +
); diff --git a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx index 3526081b0..3ff6f8bcf 100644 --- a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx @@ -1,7 +1,5 @@ -import React, { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { fetchIndexers } from 'Store/Actions/settingsActions'; +import React, { useMemo } from 'react'; +import { useSortedIndexers } from 'Settings/Indexers/useIndexers'; import FilterBuilderRowValue, { FilterBuilderRowValueProps, } from './FilterBuilderRowValue'; @@ -14,26 +12,16 @@ type IndexerFilterBuilderRowValueProps = Omit< function IndexerFilterBuilderRowValue( props: IndexerFilterBuilderRowValueProps ) { - const dispatch = useDispatch(); - - const { isPopulated, items } = useSelector( - (state: AppState) => state.settings.indexers - ); + const { data } = useSortedIndexers(); const tagList = useMemo(() => { - return items.map((item) => { + return data.map((item) => { return { id: item.id, name: item.name, }; }); - }, [items]); - - useEffect(() => { - if (!isPopulated) { - dispatch(fetchIndexers()); - } - }, [isPopulated, dispatch]); + }, [data]); return ; } diff --git a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx index 89ef35869..123d59d25 100644 --- a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; +import { useLanguages } from 'Language/useLanguages'; import FilterBuilderRowValue, { FilterBuilderRowValueProps, } from './FilterBuilderRowValue'; @@ -13,7 +12,7 @@ type LanguageFilterBuilderRowValueProps = Omit< function LanguageFilterBuilderRowValue( props: LanguageFilterBuilderRowValueProps ) { - const { items } = useSelector(createLanguagesSelector()); + const { data: items = [] } = useLanguages(); return ; } diff --git a/frontend/src/Components/Filter/Builder/ReleaseTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/ReleaseTypeFilterBuilderRowValue.tsx new file mode 100644 index 000000000..d76c28110 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/ReleaseTypeFilterBuilderRowValue.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue, { + FilterBuilderRowValueProps, +} from './FilterBuilderRowValue'; + +const releaseTypeList = [ + { + id: 'unknown', + get name() { + return translate('Unknown'); + }, + }, + { + id: 'singleEpisode', + get name() { + return translate('SingleEpisode'); + }, + }, + { + id: 'multiEpisode', + get name() { + return translate('MultiEpisode'); + }, + }, + { + id: 'seasonPack', + get name() { + return translate('SeasonPack'); + }, + }, +]; + +type ReleaseTypeFilterBuilderRowValueProps = Omit< + FilterBuilderRowValueProps, + 'tagList' +>; + +function ReleaseTypeFilterBuilderRowValue( + props: ReleaseTypeFilterBuilderRowValueProps +) { + return ; +} + +export default ReleaseTypeFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx index c01686381..dcc7e6405 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx @@ -59,7 +59,11 @@ function CustomFilter({
{label}
- + diff --git a/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx b/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx index e4f149d3c..8a7b3b749 100644 --- a/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx +++ b/frontend/src/Components/Form/Select/IndexerFlagsSelectInput.tsx @@ -1,35 +1,8 @@ -import React, { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; +import React, { useCallback, useMemo } from 'react'; +import useIndexerFlags from 'Settings/Indexers/useIndexerFlags'; import { EnhancedSelectInputChanged } from 'typings/inputs'; 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, - }; - } - ); - export interface IndexerFlagsSelectInputProps { name: string; indexerFlags: number; @@ -42,7 +15,29 @@ function IndexerFlagsSelectInput({ onChange, ...otherProps }: IndexerFlagsSelectInputProps) { - const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags)); + const { data: allIndexerFlags } = useIndexerFlags(); + + const value = useMemo( + () => + allIndexerFlags.reduce((acc: number[], { id }) => { + // eslint-disable-next-line no-bitwise + if ((indexerFlags & id) === id) { + acc.push(id); + } + + return acc; + }, []), + [allIndexerFlags, indexerFlags] + ); + + const values = useMemo( + () => + allIndexerFlags.map(({ id, name }) => ({ + key: id, + value: name, + })), + [allIndexerFlags] + ); const handleChange = useCallback( (change: EnhancedSelectInputChanged) => { diff --git a/frontend/src/Components/Form/Select/IndexerSelectInput.tsx b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx index 14a6df8ec..a66b423db 100644 --- a/frontend/src/Components/Form/Select/IndexerSelectInput.tsx +++ b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx @@ -1,43 +1,9 @@ -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 React, { useMemo } from 'react'; +import { useSortedIndexers } from 'Settings/Indexers/useIndexers'; 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, - }; - } - ); -} - export interface IndexerSelectInputProps { name: string; value: number | number[]; @@ -51,16 +17,23 @@ function IndexerSelectInput({ includeAny = false, onChange, }: IndexerSelectInputProps) { - const dispatch = useDispatch(); - const { isFetching, isPopulated, values } = useSelector( - createIndexersSelector(includeAny) - ); + const { isFetching, data } = useSortedIndexers(); - useEffect(() => { - if (!isPopulated) { - dispatch(fetchIndexers()); + const values = useMemo(() => { + const indexerOptions = data.map((indexer) => ({ + key: indexer.id, + value: indexer.name, + })); + + if (includeAny) { + indexerOptions.unshift({ + key: 0, + value: `(${translate('Any')})`, + }); } - }, [isPopulated, dispatch]); + + return indexerOptions; + }, [data, includeAny]); return ( void; } @@ -28,16 +30,17 @@ export default function LanguageSelectInput({ includeNoChange = false, includeNoChangeDisabled, includeMixed = false, + includeAny = true, + includeOriginal = false, + includeUnknown = false, onChange, ...otherProps }: LanguageSelectInputProps) { - const { items } = useSelector( - createLanguagesSelector({ - Any: true, - Original: true, - Unknown: true, - }) - ); + const { data: items = [] } = useFilteredLanguages({ + Any: !includeAny, + Original: !includeOriginal, + Unknown: !includeUnknown, + }); const values = useMemo(() => { const result: EnhancedSelectInputValue[] = items.map( diff --git a/frontend/src/Components/Form/Tag/TagInputTag.tsx b/frontend/src/Components/Form/Tag/TagInputTag.tsx index 7b549767c..3612258dc 100644 --- a/frontend/src/Components/Form/Tag/TagInputTag.tsx +++ b/frontend/src/Components/Form/Tag/TagInputTag.tsx @@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import MiddleTruncate from 'Components/MiddleTruncate'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import { TagBase } from './TagInput'; import styles from './TagInputTag.css'; @@ -66,6 +67,7 @@ function TagInputTag({ diff --git a/frontend/src/Components/Form/TextInput.tsx b/frontend/src/Components/Form/TextInput.tsx index 9cc2666a3..d7bf800c5 100644 --- a/frontend/src/Components/Form/TextInput.tsx +++ b/frontend/src/Components/Form/TextInput.tsx @@ -2,11 +2,13 @@ import classNames from 'classnames'; import React, { ChangeEvent, FocusEvent, + forwardRef, SyntheticEvent, useCallback, useEffect, useRef, } from 'react'; +import useCombinedRefs from 'Helpers/Hooks/useCombinedRefs'; import { FileInputChanged, InputChanged } from 'typings/inputs'; import styles from './TextInput.css'; @@ -39,147 +41,153 @@ export interface FileInputProps extends CommonTextInputProps { onChange: (change: FileInputChanged) => void; } -function TextInput({ - className = styles.input, - type = 'text', - readOnly = false, - autoFocus = false, - placeholder, - name, - value = '', - hasError, - hasWarning, - hasButton, - step, - min, - max, - onBlur, - onFocus, - onCopy, - onChange, - onSelectionChange, -}: TextInputProps | FileInputProps): JSX.Element { - const inputRef = useRef(null); - const selectionTimeout = useRef>(); - const selectionStart = useRef(); - const selectionEnd = useRef(); - const isMouseTarget = useRef(false); +const TextInput = forwardRef( + ( + { + className = styles.input, + type = 'text', + readOnly = false, + autoFocus = false, + placeholder, + name, + value = '', + hasError, + hasWarning, + hasButton, + step, + min, + max, + onBlur, + onFocus, + onCopy, + onChange, + onSelectionChange, + }: TextInputProps | FileInputProps, + ref + ) => { + const inputRef = useRef(null); + const combinedRef = useCombinedRefs(ref, inputRef); + const selectionTimeout = useRef>(); + const selectionStart = useRef(); + const selectionEnd = useRef(); + const isMouseTarget = useRef(false); - const selectionChanged = useCallback(() => { - if (selectionTimeout.current) { - clearTimeout(selectionTimeout.current); - } - - selectionTimeout.current = setTimeout(() => { - if (!inputRef.current) { - return; + const selectionChanged = useCallback(() => { + if (selectionTimeout.current) { + clearTimeout(selectionTimeout.current); } - const start = inputRef.current.selectionStart; - const end = inputRef.current.selectionEnd; + selectionTimeout.current = setTimeout(() => { + if (!inputRef.current) { + return; + } - const selectionChanged = - selectionStart.current !== start || selectionEnd.current !== end; + const start = inputRef.current.selectionStart; + const end = inputRef.current.selectionEnd; - selectionStart.current = start; - selectionEnd.current = end; + const selectionChanged = + selectionStart.current !== start || selectionEnd.current !== end; - if (selectionChanged) { - onSelectionChange?.(start, end); + selectionStart.current = start; + selectionEnd.current = end; + + if (selectionChanged) { + onSelectionChange?.(start, end); + } + }, 10); + }, [onSelectionChange]); + + const handleChange = useCallback( + (event: ChangeEvent) => { + onChange({ + name, + value: event.target.value, + files: type === 'file' ? event.target.files : undefined, + }); + }, + [name, type, onChange] + ); + + const handleFocus = useCallback( + (event: FocusEvent) => { + 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(); } - }, 10); - }, [onSelectionChange]); + }, [type]); - const handleChange = useCallback( - (event: ChangeEvent) => { - onChange({ - name, - value: event.target.value, - files: type === 'file' ? event.target.files : undefined, - }); - }, - [name, type, onChange] - ); + const handleDocumentMouseUp = useCallback(() => { + if (isMouseTarget.current) { + selectionChanged(); + } - const handleFocus = useCallback( - (event: FocusEvent) => { - onFocus?.(event); + isMouseTarget.current = false; + }, [selectionChanged]); - selectionChanged(); - }, - [selectionChanged, onFocus] - ); + useEffect(() => { + window.addEventListener('mouseup', handleDocumentMouseUp); - const handleKeyUp = useCallback(() => { - selectionChanged(); - }, [selectionChanged]); + return () => { + window.removeEventListener('mouseup', handleDocumentMouseUp); + }; + }, [handleDocumentMouseUp]); - const handleMouseDown = useCallback(() => { - isMouseTarget.current = true; - }, []); + useEffect(() => { + return () => { + clearTimeout(selectionTimeout.current); + }; + }, []); - 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 ( - - ); -} + return ( + + ); + } +); export default TextInput; diff --git a/frontend/src/Components/Link/ClipboardButton.css b/frontend/src/Components/Link/ClipboardButton.css index 438489155..a524dec26 100644 --- a/frontend/src/Components/Link/ClipboardButton.css +++ b/frontend/src/Components/Link/ClipboardButton.css @@ -4,6 +4,10 @@ position: relative; } +.buttonText { + margin: 0 5px; +} + .stateIconContainer { position: absolute; top: 50%; diff --git a/frontend/src/Components/Link/ClipboardButton.css.d.ts b/frontend/src/Components/Link/ClipboardButton.css.d.ts index c1ad078d8..8a5347352 100644 --- a/frontend/src/Components/Link/ClipboardButton.css.d.ts +++ b/frontend/src/Components/Link/ClipboardButton.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'button': string; + 'buttonText': string; 'clipboardIconContainer': string; 'showStateIcon': string; 'stateIconContainer': string; diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx index dfce115ac..3b9e1beba 100644 --- a/frontend/src/Components/Link/ClipboardButton.tsx +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -8,6 +8,7 @@ import styles from './ClipboardButton.css'; export interface ClipboardButtonProps extends Omit { value: string; + label?: string | number; } export type ClipboardState = 'success' | 'error' | null; @@ -15,6 +16,7 @@ export type ClipboardState = 'success' | 'error' | null; export default function ClipboardButton({ id, value, + label, className = styles.button, ...otherProps }: ClipboardButtonProps) { @@ -68,6 +70,7 @@ export default function ClipboardButton({ ) : null} + {label ? {label} : null} diff --git a/frontend/src/Components/Link/IconButton.tsx b/frontend/src/Components/Link/IconButton.tsx index b6951c00c..52ac0b036 100644 --- a/frontend/src/Components/Link/IconButton.tsx +++ b/frontend/src/Components/Link/IconButton.tsx @@ -1,7 +1,6 @@ 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'; @@ -26,7 +25,6 @@ export default function IconButton({ className, otherProps.isDisabled && styles.isDisabled )} - aria-label={translate('TableOptionsButton')} {...otherProps} > ); diff --git a/frontend/src/Components/Menu/Menu.tsx b/frontend/src/Components/Menu/Menu.tsx index 3fa0075ee..34d79a099 100644 --- a/frontend/src/Components/Menu/Menu.tsx +++ b/frontend/src/Components/Menu/Menu.tsx @@ -157,7 +157,7 @@ function Menu({ {React.cloneElement(childrenArray[1] as ReactElement, { forwardedRef: refs.setFloating, style: { - maxHeight, + maxHeight: enforceMaxHeight ? maxHeight : undefined, ...floatingStyles, }, isOpen: isMenuOpen, diff --git a/frontend/src/Components/Modal/Modal.tsx b/frontend/src/Components/Modal/Modal.tsx index cfc157f93..e3b54beb6 100644 --- a/frontend/src/Components/Modal/Modal.tsx +++ b/frontend/src/Components/Modal/Modal.tsx @@ -15,6 +15,7 @@ import { Size } from 'Helpers/Props/sizes'; import { isIOS } from 'Utilities/browser'; import * as keyCodes from 'Utilities/Constants/keyCodes'; import { setScrollLock } from 'Utilities/scrollLock'; +import { ModalContext } from './ModalContext'; import ModalError from './ModalError'; import styles from './Modal.css'; @@ -163,26 +164,36 @@ function Modal({ return null; } + const headerId = `${modalId}-header`; + return ReactDOM.createPortal( - -
-
-
- + +
+
+
- {children} - + + {children} + +
-
- , + + , node! ); } diff --git a/frontend/src/Components/Modal/ModalContext.ts b/frontend/src/Components/Modal/ModalContext.ts new file mode 100644 index 000000000..405d5ddd1 --- /dev/null +++ b/frontend/src/Components/Modal/ModalContext.ts @@ -0,0 +1,11 @@ +import { createContext, useContext } from 'react'; + +interface ModalContextValue { + headerId: string; +} + +export const ModalContext = createContext({ headerId: '' }); + +export function useModalContext() { + return useContext(ModalContext); +} diff --git a/frontend/src/Components/Modal/ModalHeader.tsx b/frontend/src/Components/Modal/ModalHeader.tsx index 86f2c9ac1..e90ad2f0b 100644 --- a/frontend/src/Components/Modal/ModalHeader.tsx +++ b/frontend/src/Components/Modal/ModalHeader.tsx @@ -1,4 +1,5 @@ import React, { ForwardedRef, forwardRef, ReactNode } from 'react'; +import { useModalContext } from './ModalContext'; import styles from './ModalHeader.css'; interface ModalHeaderProps extends React.HTMLAttributes { @@ -10,8 +11,15 @@ const ModalHeader = forwardRef( { children, ...otherProps }: ModalHeaderProps, ref: ForwardedRef ) => { + const { headerId } = useModalContext(); + return ( -
+
{children}
); diff --git a/frontend/src/Components/MonitorToggleButton.tsx b/frontend/src/Components/MonitorToggleButton.tsx index 1c1fcbbeb..36d95903f 100644 --- a/frontend/src/Components/MonitorToggleButton.tsx +++ b/frontend/src/Components/MonitorToggleButton.tsx @@ -54,6 +54,7 @@ function MonitorToggleButton(props: MonitorToggleButtonProps) { name={iconName} size={size} title={title} + aria-label={title} isDisabled={isDisabled} isSpinning={isSaving} {...otherProps} diff --git a/frontend/src/Components/Page/Header/PageHeader.tsx b/frontend/src/Components/Page/Header/PageHeader.tsx index c63447bbf..54a96697c 100644 --- a/frontend/src/Components/Page/Header/PageHeader.tsx +++ b/frontend/src/Components/Page/Header/PageHeader.tsx @@ -55,6 +55,7 @@ function PageHeader() {
diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx index 9c3ffcd0a..89d756645 100644 --- a/frontend/src/Components/Page/PageContentBody.tsx +++ b/frontend/src/Components/Page/PageContentBody.tsx @@ -1,5 +1,6 @@ import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react'; import Scroller, { OnScroll } from 'Components/Scroller/Scroller'; +import useScrollPosition from 'Helpers/Hooks/useScrollPosition'; import { isLocked } from 'Utilities/scrollLock'; import styles from './PageContentBody.css'; @@ -7,7 +8,7 @@ interface PageContentBodyProps { className?: string; innerClassName?: string; children: ReactNode; - initialScrollTop?: number; + scrollPositionKey?: string; onScroll?: (payload: OnScroll) => void; } @@ -17,26 +18,32 @@ const PageContentBody = forwardRef( className = styles.contentBody, innerClassName = styles.innerContentBody, children, + scrollPositionKey, onScroll, - ...otherProps } = props; - const onScrollWrapper = useCallback( + const { initialScrollTop, onScroll: onScrollMemo } = + useScrollPosition(scrollPositionKey); + + const handleScroll = useCallback( (payload: OnScroll) => { - if (onScroll && !isLocked()) { - onScroll(payload); + if (isLocked()) { + return; } + + onScrollMemo(payload); + onScroll?.(payload); }, - [onScroll] + [onScroll, onScrollMemo] ); return (
{children}
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx index e2f5460f9..a2245daee 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx @@ -435,10 +435,11 @@ function PageSidebar() { const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; return ( -
{isSmallScreen ? (
@@ -521,7 +522,7 @@ function PageSidebar() { -
+ ); } diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.tsx b/frontend/src/Components/Page/Sidebar/PageSidebarItem.tsx index 37d9bafa0..c2b8cfff1 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.tsx +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.tsx @@ -46,11 +46,12 @@ function PageSidebarItem({ isActive && styles.isActiveLink )} to={to} + aria-current={isActive ? 'page' : undefined} onPress={handlePress} > {!!iconName && ( - + )} diff --git a/frontend/src/Components/SignalRListener.tsx b/frontend/src/Components/SignalRListener.tsx index ce6ca51bf..de6d8e442 100644 --- a/frontend/src/Components/SignalRListener.tsx +++ b/frontend/src/Components/SignalRListener.tsx @@ -14,6 +14,8 @@ import Episode from 'Episode/Episode'; import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery'; import Series from 'Series/Series'; +import { IndexerModel } from 'Settings/Indexers/useIndexers'; +import { NotificationModel } from 'Settings/Notifications/useConnections'; import { removeItem, updateItem } from 'Store/Actions/baseActions'; import { repopulatePage } from 'Utilities/pagePopulator'; import SignalRLogger from 'Utilities/SignalRLogger'; @@ -141,30 +143,11 @@ function SignalRListener() { if (body.action === 'updated') { const updatedItem = body.resource as Episode; - queryClient.setQueriesData( - { queryKey: ['/episode'] }, - (oldData: Episode[] | undefined) => { - if (!oldData) { - return oldData; - } - - const itemIndex = oldData.findIndex( - (item) => item.id === updatedItem.id - ); - - // Don't add episode if not found - if (itemIndex === -1) { - return oldData; - } - - return oldData.map((item) => { - if (item.id === updatedItem.id) { - return updatedItem; - } - - return item; - }); - } + updateQueryClientItem( + queryClient, + ['/episode'], + updatedItem, + false // Don't add the episode to the list if it doesn't exist. Episodes should already be in the list since they are included in the series details. ); } @@ -179,30 +162,11 @@ function SignalRListener() { if (body.action === 'updated') { const updatedItem = body.resource as EpisodeFile; - queryClient.setQueriesData( - { queryKey: ['/episodeFile'] }, - (oldData: EpisodeFile[] | undefined) => { - if (!oldData) { - return oldData; - } - - const itemIndex = oldData.findIndex( - (item) => item.id === updatedItem.id - ); - - // Add episode file to the end - if (itemIndex === -1) { - return [...oldData, updatedItem]; - } - - return oldData.map((item) => { - if (item.id === updatedItem.id) { - return updatedItem; - } - - return item; - }); - } + updateQueryClientItem( + queryClient, + ['/episodeFile'], + updatedItem, + true // Add the episode file to the list if it doesn't exist. This can happen when an episode file is imported and wasn't previously in the list of episode files. ); // Repopulate the page to handle recently imported file @@ -210,24 +174,7 @@ function SignalRListener() { } else if (body.action === 'deleted') { const id = body.resource.id; - queryClient.setQueriesData( - { queryKey: ['/episodeFile'] }, - (oldData: EpisodeFile[] | undefined) => { - if (!oldData) { - return oldData; - } - - const itemIndex = oldData.findIndex((item) => item.id === id); - - // Add episode file to the end - if (itemIndex === -1) { - return oldData; - } - - return oldData.filter((item) => item.id !== id); - } - ); - + removeQueryClientItem(queryClient, ['/episodeFile'], id); repopulatePage('episodeFileDeleted'); } @@ -256,34 +203,39 @@ function SignalRListener() { } if (name === 'indexer') { - const section = 'settings.indexers'; + const updatedItem = body.resource as IndexerModel; if (body.action === 'created' || body.action === 'updated') { - dispatch(updateItem({ section, ...body.resource })); + updateQueryClientItem(queryClient, ['/indexer'], updatedItem, true); } else if (body.action === 'deleted') { - dispatch(removeItem({ section, id: body.resource.id })); + removeQueryClientItem(queryClient, ['/indexer'], body.resource.id); } return; } if (name === 'metadata') { - const section = 'settings.metadata'; + const updatedItem = body.resource as ModelBase; if (body.action === 'updated') { - dispatch(updateItem({ section, ...body.resource })); + updateQueryClientItem(queryClient, ['/metadata'], updatedItem, false); } return; } - if (name === 'notification') { - const section = 'settings.notifications'; + if (name === 'connection') { + const updatedItem = body.resource as NotificationModel; if (body.action === 'created' || body.action === 'updated') { - dispatch(updateItem({ section, ...body.resource })); + updateQueryClientItem( + queryClient, + ['/connection'], + updatedItem, + body.action === 'created' // Only add the connection to the list if it was created. If it was updated and it doesn't exist in the list, it likely means the connection is disabled and shouldn't be shown in the list. + ); } else if (body.action === 'deleted') { - dispatch(removeItem({ section, id: body.resource.id })); + removeQueryClientItem(queryClient, ['/connection'], body.resource.id); } return; @@ -350,42 +302,16 @@ function SignalRListener() { if (body.action === 'updated') { const updatedItem = body.resource as Series; - queryClient.setQueryData( + updateQueryClientItem( + queryClient, ['/series'], - (oldData: Series[] | undefined) => { - if (!oldData) { - return oldData; - } - - return oldData.map((item) => { - if (item.id === updatedItem.id) { - return { - ...item, - ...updatedItem, - }; - } - - return item; - }); - } + updatedItem, + false // Don't add the series to the list if it doesn't exist. Series should already be in the list since they are included in the calendar and series details. ); repopulatePage('seriesUpdated'); } else if (body.action === 'deleted') { - dispatch(removeItem({ section: 'series', id: body.resource.id })); - - queryClient.setQueriesData( - { queryKey: ['/series'] }, - (oldData: Series[] | undefined) => { - if (!oldData) { - return oldData; - } - - return oldData.filter((item) => { - return item.id !== body.resource.id; - }); - } - ); + removeQueryClientItem(queryClient, ['/series'], body.resource.id); } return; @@ -521,3 +447,50 @@ const updatePagedItem = ( } ); }; + +const updateQueryClientItem = ( + queryClient: ReturnType, + queryKey: QueryKey, + updatedItem: T, + addMissing: boolean +) => { + queryClient.setQueriesData({ queryKey }, (oldData: T[] | undefined) => { + if (!oldData) { + return oldData; + } + + const itemIndex = oldData.findIndex((item) => item.id === updatedItem.id); + + if (itemIndex === -1 && addMissing) { + return [...oldData, updatedItem]; + } + + return oldData.map((item) => { + if (item.id === updatedItem.id) { + return updatedItem; + } + + return item; + }); + }); +}; + +const removeQueryClientItem = ( + queryClient: ReturnType, + queryKey: QueryKey, + id: T['id'] +) => { + queryClient.setQueriesData({ queryKey }, (oldData: T[] | undefined) => { + if (!oldData) { + return oldData; + } + + const itemIndex = oldData.findIndex((item) => item.id === id); + + if (itemIndex === -1) { + return oldData; + } + + return oldData.filter((item) => item.id !== id); + }); +}; diff --git a/frontend/src/Components/Table/Table.tsx b/frontend/src/Components/Table/Table.tsx index c72b68b96..995529ec2 100644 --- a/frontend/src/Components/Table/Table.tsx +++ b/frontend/src/Components/Table/Table.tsx @@ -7,6 +7,7 @@ import { icons, scrollDirections } from 'Helpers/Props'; import { SortDirection } from 'Helpers/Props/sortDirections'; import { CheckInputChanged } from 'typings/inputs'; import { TableOptionsChangePayload } from 'typings/Table'; +import translate from 'Utilities/String/translate'; import Column from './Column'; import TableHeader from './TableHeader'; import TableHeaderCell from './TableHeaderCell'; @@ -94,7 +95,10 @@ function Table({ canModifyColumns={canModifyColumns} onTableOptionChange={onTableOptionChange} > - + ); diff --git a/frontend/src/Components/Table/TableHeaderCell.tsx b/frontend/src/Components/Table/TableHeaderCell.tsx index 13b8cf0f7..9311a81a5 100644 --- a/frontend/src/Components/Table/TableHeaderCell.tsx +++ b/frontend/src/Components/Table/TableHeaderCell.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import { icons, sortDirections } from 'Helpers/Props'; @@ -41,6 +41,20 @@ function TableHeaderCell({ ? icons.SORT_ASCENDING : icons.SORT_DESCENDING; + const ariaSortValue = useMemo(() => { + if (!isSortable) { + return undefined; + } + + if (!isSorting) { + return 'none'; + } + + return sortDirection === sortDirections.ASCENDING + ? 'ascending' + : 'descending'; + }, [isSorting, sortDirection, isSortable]); + const handlePress = useCallback(() => { if (fixedSortDirection) { onSortPress?.(name, fixedSortDirection); @@ -56,14 +70,20 @@ function TableHeaderCell({ className={className} // label={typeof label === 'function' ? label() : label} title={typeof columnLabel === 'function' ? columnLabel() : columnLabel} + scope="col" + aria-sort={ariaSortValue} onPress={handlePress} > {children} - {isSorting && } + {isSorting ? ( + + ) : null} ) : ( -
+ ); } diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx b/frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx index 4c0d69339..0fd9b2ae4 100644 --- a/frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.tsx @@ -110,7 +110,7 @@ function TableOptionsModal({ const handleColumnDragEnd = useCallback( (didDrop: boolean) => { - if (didDrop && dragIndex && dropIndex !== null) { + if (didDrop && dragIndex !== null && dropIndex !== null) { const newColumns = [...columns]; const items = newColumns.splice(dragIndex, 1); newColumns.splice(dropIndex, 0, items[0]); diff --git a/frontend/src/Components/Table/TablePager.tsx b/frontend/src/Components/Table/TablePager.tsx index 411b111bf..5394ccc69 100644 --- a/frontend/src/Components/Table/TablePager.tsx +++ b/frontend/src/Components/Table/TablePager.tsx @@ -108,9 +108,10 @@ function TablePager({ isFirstPage && styles.disabledPageButton )} isDisabled={isFirstPage} + aria-label={translate('PagerGoToFirstPage')} onPress={handleFirstPagePress} > - + - +
{isShowingPageSelect ? null : ( {page} / {totalPages} @@ -153,9 +159,10 @@ function TablePager({ isLastPage && styles.disabledPageButton )} isDisabled={isLastPage} + aria-label={translate('PagerGoToNextPage')} onPress={onNextPagePress} > - + - +
diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx deleted file mode 100644 index f688a6253..000000000 --- a/frontend/src/Components/withScrollPosition.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import scrollPositions from 'Store/scrollPositions'; - -interface WrappedComponentProps { - initialScrollTop: number; -} - -interface ScrollPositionProps { - history: RouteComponentProps['history']; - location: RouteComponentProps['location']; - match: RouteComponentProps['match']; -} - -function withScrollPosition( - WrappedComponent: React.FC, - scrollPositionKey: string -) { - function ScrollPosition(props: ScrollPositionProps) { - const { history } = props; - - const initialScrollTop = - history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0; - - return ; - } - - return ScrollPosition; -} - -export default withScrollPosition; diff --git a/frontend/src/Content/Images/thetvdb-dark.png b/frontend/src/Content/Images/thetvdb-dark.png new file mode 100644 index 000000000..ab4e9b8fe Binary files /dev/null and b/frontend/src/Content/Images/thetvdb-dark.png differ diff --git a/frontend/src/Content/Images/thetvdb-light.png b/frontend/src/Content/Images/thetvdb-light.png new file mode 100644 index 000000000..7b48b6289 Binary files /dev/null and b/frontend/src/Content/Images/thetvdb-light.png differ diff --git a/frontend/src/Content/Images/thetvdb.png b/frontend/src/Content/Images/thetvdb.png deleted file mode 100644 index 1d751483b..000000000 Binary files a/frontend/src/Content/Images/thetvdb.png and /dev/null differ diff --git a/frontend/src/Episode/EpisodeSearchCell.tsx b/frontend/src/Episode/EpisodeSearchCell.tsx index 7e0f38a86..cea39a6ee 100644 --- a/frontend/src/Episode/EpisodeSearchCell.tsx +++ b/frontend/src/Episode/EpisodeSearchCell.tsx @@ -54,6 +54,7 @@ function EpisodeSearchCell({ diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.tsx b/frontend/src/Episode/History/EpisodeHistoryRow.tsx index a5c4a14f5..1503fdccb 100644 --- a/frontend/src/Episode/History/EpisodeHistoryRow.tsx +++ b/frontend/src/Episode/History/EpisodeHistoryRow.tsx @@ -128,6 +128,7 @@ function EpisodeHistoryRow({ {eventType === 'grabbed' && ( (indexerFlags & item.id) === item.id ); diff --git a/frontend/src/Episode/Summary/EpisodeFileRow.tsx b/frontend/src/Episode/Summary/EpisodeFileRow.tsx index d2bf5f4ba..58388697a 100644 --- a/frontend/src/Episode/Summary/EpisodeFileRow.tsx +++ b/frontend/src/Episode/Summary/EpisodeFileRow.tsx @@ -124,6 +124,7 @@ function EpisodeFileRow(props: EpisodeFileRowProps) { diff --git a/frontend/src/Episode/Summary/MediaInfo.tsx b/frontend/src/Episode/Summary/MediaInfo.tsx index ef5eb499a..7b2e35b84 100644 --- a/frontend/src/Episode/Summary/MediaInfo.tsx +++ b/frontend/src/Episode/Summary/MediaInfo.tsx @@ -1,13 +1,15 @@ import React from 'react'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import useLanguageName from 'Language/useLanguageName'; import MediaInfoProps from 'typings/MediaInfo'; import formatBitrate from 'Utilities/Number/formatBitrate'; import getEntries from 'Utilities/Object/getEntries'; -import getLanguageName from 'Utilities/String/getLanguageName'; import translate from 'Utilities/String/translate'; function MediaInfo(props: MediaInfoProps) { + const getLanguageName = useLanguageName(); + return ( {getEntries(props).map(([key, value]) => { @@ -21,7 +23,10 @@ function MediaInfo(props: MediaInfoProps) { if (key === 'audioStreams') { return value.map((audioStream, index) => { - const language = getLanguageName(audioStream.language); + const language = + audioStream.language === 'und' + ? translate('Unknown') + : getLanguageName(audioStream.language); let line = `${language}`; @@ -56,39 +61,50 @@ function MediaInfo(props: MediaInfoProps) { { - const language = getLanguageName(subtitleStream.language); + data={ + value.length > 0 + ? value.reduce( + ( + acc: React.ReactNode[] | null, + subtitleStream, + index + ) => { + const language = getLanguageName( + subtitleStream.language + ); - let line = `${ - subtitleStream.format?.toUpperCase() || translate('Unknown') - }`; + let line = `${ + subtitleStream.format?.toUpperCase() || + translate('Unknown') + }`; - if ( - subtitleStream.title !== undefined && - subtitleStream.title !== language - ) { - line += ` | ${subtitleStream.title}`; - } + if ( + subtitleStream.title !== undefined && + subtitleStream.title !== language + ) { + line += ` | ${subtitleStream.title}`; + } - if (subtitleStream.forced) { - line += ` | ${translate('MediaInfoForced')}`; - } + if (subtitleStream.forced) { + line += ` | ${translate('MediaInfoForced')}`; + } - if (subtitleStream.hearingImpaired) { - line += ` | ${translate('MediaInfoHearingImpaired')}`; - } + if (subtitleStream.hearingImpaired) { + line += ` | ${translate('MediaInfoHearingImpaired')}`; + } - const curr = ( - - {language} - - ); + const curr = ( + + {language} + + ); - return acc === null ? [curr] : [acc, ' / ', curr]; - }, - null - )} + return acc === null ? [curr] : [acc, ' / ', curr]; + }, + null + ) + : translate('None') + } /> ); } diff --git a/frontend/src/EpisodeFile/MediaInfo.tsx b/frontend/src/EpisodeFile/MediaInfo.tsx index f737e6fb4..de137f0d2 100644 --- a/frontend/src/EpisodeFile/MediaInfo.tsx +++ b/frontend/src/EpisodeFile/MediaInfo.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import getLanguageName from 'Utilities/String/getLanguageName'; +import useLanguageName from 'Language/useLanguageName'; import translate from 'Utilities/String/translate'; import { useEpisodeFile } from './EpisodeFileProvider'; -function formatLanguages(languages: string[] | undefined) { +function formatLanguages( + languages: string[] | undefined, + getLanguageName: (code: string) => string +) { if (!languages) { return null; } @@ -43,6 +46,7 @@ interface MediaInfoProps { } function MediaInfo({ episodeFileId, type }: MediaInfoProps) { + const getLanguageName = useLanguageName(); const episodeFile = useEpisodeFile(episodeFileId); if (!episodeFile?.mediaInfo) { @@ -76,11 +80,17 @@ function MediaInfo({ episodeFileId, type }: MediaInfoProps) { } if (type === 'audioLanguages') { - return formatLanguages(audioStreams.map(({ language }) => language)); + return formatLanguages( + audioStreams.map(({ language }) => language), + getLanguageName + ); } if (type === 'subtitles') { - return formatLanguages(subtitleStreams.map(({ language }) => language)); + return formatLanguages( + subtitleStreams.map(({ language }) => language), + getLanguageName + ); } if (type === 'video') { diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx index 747942851..c4021b82a 100644 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx @@ -1,5 +1,4 @@ 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'; @@ -16,31 +15,22 @@ import { authenticationMethodOptions, authenticationRequiredOptions, } from 'Settings/General/SecuritySettings'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { - fetchGeneralSettings, - saveGeneralSettings, - setGeneralSettingsValue, -} from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { useManageGeneralSettings } from 'Settings/General/useGeneralSettings'; import useSystemStatus from 'System/Status/useSystemStatus'; 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 { refetch: refetchStatus } = useSystemStatus(); + const { settings, isFetched, error, isSaving, saveSettings, updateSetting } = + useManageGeneralSettings(); + const { authenticationMethod, authenticationRequired, @@ -51,20 +41,12 @@ export default function AuthenticationRequiredModalContent() { const wasSaving = usePrevious(isSaving); - useEffect(() => { - dispatch(fetchGeneralSettings()); - - return () => { - dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); - }; - }, [dispatch]); - const onInputChange = useCallback( - (args: InputChanged) => { - // @ts-expect-error Actions aren't typed - dispatch(setGeneralSettingsValue(args)); + (change: InputChanged) => { + // @ts-expect-error input change events aren't typed + updateSetting(change.name, change.value); }, - [dispatch] + [updateSetting] ); const authenticationEnabled = @@ -79,8 +61,8 @@ export default function AuthenticationRequiredModalContent() { }, [isSaving, wasSaving, refetchStatus]); const onPress = useCallback(() => { - dispatch(saveGeneralSettings()); - }, [dispatch]); + saveSettings(); + }, [saveSettings]); return ( @@ -91,7 +73,7 @@ export default function AuthenticationRequiredModalContent() { {translate('AuthenticationRequiredWarning')} - {isPopulated && !error ? ( + {isFetched && !error ? (
{translate('AuthenticationMethod')} @@ -177,7 +159,7 @@ export default function AuthenticationRequiredModalContent() {
) : null} - {!isPopulated && !error ? : null} + {!isFetched && !error ? : null} diff --git a/frontend/src/Helpers/Hooks/useApiMutation.ts b/frontend/src/Helpers/Hooks/useApiMutation.ts index 641db3d50..36999380e 100644 --- a/frontend/src/Helpers/Hooks/useApiMutation.ts +++ b/frontend/src/Helpers/Hooks/useApiMutation.ts @@ -1,5 +1,6 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query'; import { useMemo } from 'react'; +import ModelBase from 'App/ModelBase'; import { ValidationFailures } from 'Store/Selectors/selectSettings'; import { ValidationError, @@ -71,3 +72,16 @@ export function getValidationFailures( } ); } + +export function addOrUpdateQueryClientItem< + T extends ModelBase, + K extends keyof T +>(oldData: T[] = [], newItem: T, key: K) { + const existingIndex = oldData.findIndex((item) => item[key] === newItem[key]); + + if (existingIndex === -1) { + return [...oldData, newItem]; + } + + return oldData.map((item) => (item[key] === newItem[key] ? newItem : item)); +} diff --git a/frontend/src/Helpers/Hooks/useAppPage.ts b/frontend/src/Helpers/Hooks/useAppPage.ts index cd554f188..24115e1d6 100644 --- a/frontend/src/Helpers/Hooks/useAppPage.ts +++ b/frontend/src/Helpers/Hooks/useAppPage.ts @@ -5,41 +5,42 @@ import AppState from 'App/State/AppState'; import { useTranslations } from 'App/useTranslations'; import useCommands from 'Commands/useCommands'; import useCustomFilters from 'Filters/useCustomFilters'; +import { useInitializeLanguage } from 'Language/useLanguageName'; +import { useLanguages } from 'Language/useLanguages'; import useSeries from 'Series/useSeries'; +import useIndexerFlags from 'Settings/Indexers/useIndexerFlags'; import { useQualityProfiles } from 'Settings/Profiles/Quality/useQualityProfiles'; import { useUiSettings } from 'Settings/UI/useUiSettings'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; -import { - fetchImportLists, - fetchIndexerFlags, - fetchLanguages, -} from 'Store/Actions/settingsActions'; +import { fetchImportLists } from 'Store/Actions/settingsActions'; import useSystemStatus from 'System/Status/useSystemStatus'; import useTags from 'Tags/useTags'; import { ApiError } from 'Utilities/Fetch/fetchJson'; const createErrorsSelector = ({ customFiltersError, + indexerFlagsError, systemStatusError, tagsError, translationsError, uiSettingsError, seriesError, qualityProfilesError, + languagesError, }: { customFiltersError: ApiError | null; + indexerFlagsError: ApiError | null; systemStatusError: ApiError | null; tagsError: ApiError | null; translationsError: ApiError | null; uiSettingsError: ApiError | null; seriesError: ApiError | null; qualityProfilesError: ApiError | null; + languagesError: ApiError | null; }) => createSelector( - (state: AppState) => state.settings.languages.error, (state: AppState) => state.settings.importLists.error, - (state: AppState) => state.settings.indexerFlags.error, - (languagesError, importListsError, indexerFlagsError) => { + (importListsError) => { const hasError = !!( customFiltersError || seriesError || @@ -76,11 +77,12 @@ const useAppPage = () => { const dispatch = useDispatch(); useCommands(); + useInitializeLanguage(); const { isFetched: isCustomFiltersFetched, error: customFiltersError } = useCustomFilters(); - const { isSuccess: isSeriesFetched, error: seriesError } = useSeries(); + const { isFetched: isSeriesFetched, error: seriesError } = useSeries(); const { isFetched: isSystemStatusFetched, error: systemStatusError } = useSystemStatus(); @@ -96,32 +98,39 @@ const useAppPage = () => { const { isFetched: isQualityProfilesFetched, error: qualityProfilesError } = useQualityProfiles(); + const { isFetched: isLanguagesFetched, error: languagesError } = + useLanguages(); + + const { isFetched: isIndexerFlagsFetched, error: indexerFlagsError } = + useIndexerFlags(); + const isAppStatePopulated = useSelector( - (state: AppState) => - state.settings.languages.isPopulated && - state.settings.importLists.isPopulated && - state.settings.indexerFlags.isPopulated + (state: AppState) => state.settings.importLists.isPopulated ); const isPopulated = isAppStatePopulated && isCustomFiltersFetched && + isIndexerFlagsFetched && isSeriesFetched && isSystemStatusFetched && isTagsFetched && isTranslationsFetched && isUiSettingsFetched && - isQualityProfilesFetched; + isQualityProfilesFetched && + isLanguagesFetched; const { hasError, errors } = useSelector( createErrorsSelector({ customFiltersError, + indexerFlagsError, seriesError, systemStatusError, tagsError, translationsError, uiSettingsError, qualityProfilesError, + languagesError, }) ); @@ -140,9 +149,7 @@ const useAppPage = () => { useEffect(() => { dispatch(fetchCustomFilters()); - dispatch(fetchLanguages()); dispatch(fetchImportLists()); - dispatch(fetchIndexerFlags()); }, [dispatch]); return useMemo(() => { diff --git a/frontend/src/Helpers/Hooks/useCombinedRefs.ts b/frontend/src/Helpers/Hooks/useCombinedRefs.ts new file mode 100644 index 000000000..00ee45d29 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useCombinedRefs.ts @@ -0,0 +1,39 @@ +import { ForwardedRef, useCallback, useRef } from 'react'; + +type OptionalRef = ForwardedRef | undefined; + +function setRef(ref: OptionalRef, value: T | null) { + if (typeof ref === 'function') { + ref(value); + } else if (ref) { + ref.current = value; + } +} + +function useCombinedRefs(...refs: OptionalRef[]) { + const previousRefs = useRef[]>([]); + + return useCallback((value: T | null) => { + let index = 0; + for (; index < refs.length; index++) { + const ref = refs[index]; + const prev = previousRefs.current[index]; + + if (prev !== ref) { + setRef(prev, null); + } + setRef(ref, value); + } + + for (; index < previousRefs.current.length; index++) { + const prev = previousRefs.current[index]; + setRef(prev, null); + } + + previousRefs.current = refs; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, refs); +} + +export default useCombinedRefs; diff --git a/frontend/src/Helpers/Hooks/usePage.ts b/frontend/src/Helpers/Hooks/usePage.ts index af30c28a0..e17d94dec 100644 --- a/frontend/src/Helpers/Hooks/usePage.ts +++ b/frontend/src/Helpers/Hooks/usePage.ts @@ -7,6 +7,7 @@ interface PageStore { cutoffUnmet: number; events: number; history: number; + importListExclusion: number; missing: number; queue: number; } @@ -16,6 +17,7 @@ const pageStore = create(() => ({ cutoffUnmet: 1, events: 1, history: 1, + importListExclusion: 1, missing: 1, queue: 1, })); diff --git a/frontend/src/Helpers/Hooks/useScrollPosition.ts b/frontend/src/Helpers/Hooks/useScrollPosition.ts new file mode 100644 index 000000000..25e5133e0 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useScrollPosition.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router'; +import { OnScroll } from 'Components/Scroller/Scroller'; +import scrollPositions from 'Store/scrollPositions'; + +function useScrollPosition(key?: string) { + const { pathname } = useLocation(); + const { action } = useHistory(); + + // Reset window scroll on PUSH/REPLACE (mobile's scroll container). + // Reset the scroll position unless we're going back, this will allow the scroll + // position to reset when moving forward (PUSH/REPLACE) and restore when + // moving backwards (POP). + useEffect(() => { + if (action !== 'POP') { + window.scrollTo(0, 0); + } + }, [pathname, action]); + + const initialScrollTop = useMemo( + () => (key && action === 'POP' ? scrollPositions[key] ?? 0 : 0), + [key, action] + ); + + const onScroll = useCallback( + ({ scrollTop }: OnScroll) => { + if (key) { + scrollPositions[key] = scrollTop; + } + }, + [key] + ); + + return { initialScrollTop, onScroll }; +} + +export default useScrollPosition; diff --git a/frontend/src/Helpers/Hooks/useTheme.ts b/frontend/src/Helpers/Hooks/useTheme.ts index 970b5ae7e..96750cba1 100644 --- a/frontend/src/Helpers/Hooks/useTheme.ts +++ b/frontend/src/Helpers/Hooks/useTheme.ts @@ -2,10 +2,18 @@ import { useEffect, useState } from 'react'; import { useUiSettingsValues } from 'Settings/UI/useUiSettings'; import themes from 'Styles/Themes'; -const useTheme = () => { +const useTheme = (): 'dark' | 'light' => { const { theme } = useUiSettingsValues(); const selectedTheme = theme ?? window.Sonarr.theme; - const [resolvedTheme, setResolvedTheme] = useState(selectedTheme); + const [resolvedTheme, setResolvedTheme] = useState(() => { + if (selectedTheme === 'auto') { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + } + + return selectedTheme; + }); useEffect(() => { if (selectedTheme !== 'auto') { diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.ts b/frontend/src/Helpers/Props/filterBuilderValueTypes.ts index 4a12865c2..0110b76b1 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.ts +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.ts @@ -10,6 +10,7 @@ export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; export const QUEUE_STATUS = 'queueStatus'; export const MONITORED_STATUS = 'monitoredStatus'; +export const RELEASE_TYPES = 'releaseTypes'; export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; export const SERIES_TYPES = 'seriesType'; @@ -28,6 +29,7 @@ export type FilterBuildValueType = | 'qualityProfile' | 'queueStatus' | 'monitoredStatus' + | 'releaseTypes' | 'series' | 'seriesStatus' | 'seriesType' diff --git a/frontend/src/Helpers/Props/icons.ts b/frontend/src/Helpers/Props/icons.ts index 4f0f24f73..7550e1dd4 100644 --- a/frontend/src/Helpers/Props/icons.ts +++ b/frontend/src/Helpers/Props/icons.ts @@ -68,6 +68,7 @@ import { faFolderOpen as fasFolderOpen, faFolderTree as farFolderTree, faForward as fasForward, + faGlobe as fasGlobe, faHeart as fasHeart, faHistory as fasHistory, faHome as fasHome, @@ -166,6 +167,7 @@ export const FOOTNOTE = fasAsterisk; export const FOLDER = farFolder; export const FOLDER_OPEN = fasFolderOpen; export const GENRE = fasTheaterMasks; +export const GLOBE = fasGlobe; export const GROUP = farObjectGroup; export const HEALTH = fasMedkit; export const HEART = fasHeart; diff --git a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx index 7f1b3d393..b72a5faf7 100644 --- a/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx +++ b/frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx @@ -33,6 +33,7 @@ function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css index b15e0196e..0dd44621d 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -18,6 +18,7 @@ .leftButtons, .rightButtons { display: flex; + align-items: center; flex-wrap: wrap; min-width: 0; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index 86bdeb23f..85f94c546 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -500,6 +500,9 @@ function InteractiveImportModalContentInner( return; } + const seenEpisodeIds = new Set(); + let hasDuplicateEpisodes = false; + items.forEach((item) => { const isSelected = selectedIds.indexOf(item.id) > -1; @@ -552,6 +555,19 @@ function InteractiveImportModalContentInner( return; } + if (!hasDuplicateEpisodes) { + for (const episode of episodes) { + const hasAlreadySeen = seenEpisodeIds.has(episode.id); + + seenEpisodeIds.add(episode.id); + + if (hasAlreadySeen) { + hasDuplicateEpisodes = true; + return; + } + } + } + setInteractiveImportErrorMessage(null); if (episodeFileId) { @@ -587,6 +603,14 @@ function InteractiveImportModalContentInner( } }); + if (hasDuplicateEpisodes) { + setInteractiveImportErrorMessage( + translate('InteractiveImportDuplicateEpisodes') + ); + + return; + } + let shouldClose = false; if (existingFiles.length) { @@ -953,13 +977,13 @@ function InteractiveImportModalContentInner(
- - - {interactiveImportErrorMessage && ( + {interactiveImportErrorMessage ? ( {interactiveImportErrorMessage} - )} + ) : null} + +
diff --git a/frontend/src/Series/History/SeriesHistoryRow.tsx b/frontend/src/Series/History/SeriesHistoryRow.tsx index c1d82ae26..8968f234d 100644 --- a/frontend/src/Series/History/SeriesHistoryRow.tsx +++ b/frontend/src/Series/History/SeriesHistoryRow.tsx @@ -158,6 +158,7 @@ function SeriesHistoryRow({ {eventType === 'grabbed' ? ( + + {translate('OriginalCountry')} + + + + {translate('AverageSizePerEpisode')} + + diff --git a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx index e378eab1d..03c9f98f6 100644 --- a/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx +++ b/frontend/src/Series/Index/Posters/SeriesIndexPoster.tsx @@ -103,6 +103,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { status, path, titleSlug, + originalCountry, originalLanguage, network, nextAiring, @@ -161,6 +162,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { className={styles.action} name={icons.EDIT} title={translate('EditSeries')} + aria-label={translate('EditSeries')} tabIndex={-1} onPress={onEditSeriesPress} /> @@ -256,6 +258,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) { ) : null} @@ -59,6 +64,14 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) { ); } + if (sortKey === 'originalCountry' && !!originalCountryName) { + return ( +
+ {originalCountryName} +
+ ); + } + if (sortKey === 'originalLanguage' && !!originalLanguage?.name) { return (
diff --git a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx index ae270078a..03c58ebeb 100644 --- a/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Delete/DeleteSeriesModalContent.tsx @@ -1,6 +1,4 @@ -import { orderBy } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import { useSelect } from 'App/Select/SelectContext'; +import React, { useCallback, useState } from 'react'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; @@ -10,15 +8,15 @@ 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 Series from 'Series/Series'; import { setSeriesDeleteOptions, useSeriesDeleteOptions, } from 'Series/seriesOptionsStore'; -import useSeries, { useBulkDeleteSeries } from 'Series/useSeries'; +import { useBulkDeleteSeries } from 'Series/useSeries'; import { InputChanged } from 'typings/inputs'; -import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; +import SeriesDeleteList from './SeriesDeleteList'; +import useSelectedSeriesStats from './useSelectedSeriesStats'; import styles from './DeleteSeriesModalContent.css'; export interface DeleteSeriesModalContentProps { @@ -29,19 +27,10 @@ function DeleteSeriesModalContent({ onModalClose, }: DeleteSeriesModalContentProps) { const { addImportListExclusion } = useSeriesDeleteOptions(); - const { data: allSeries } = useSeries(); const { bulkDeleteSeries } = useBulkDeleteSeries(); const [deleteFiles, setDeleteFiles] = useState(false); - const { useSelectedIds } = useSelect(); - const seriesIds = useSelectedIds(); - - const series = useMemo((): Series[] => { - const seriesList = seriesIds.map((id) => { - return allSeries.find((s) => s.id === id); - }) as Series[]; - - return orderBy(seriesList, ['sortTitle']); - }, [allSeries, seriesIds]); + const { series, seriesIds, totalEpisodeFileCount, totalSizeOnDisk } = + useSelectedSeriesStats(); const onDeleteFilesChange = useCallback( ({ value }: InputChanged) => { @@ -78,23 +67,6 @@ function DeleteSeriesModalContent({ onModalClose, ]); - const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { - return series.reduce( - (acc, { statistics = {} }) => { - const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; - - acc.totalEpisodeFileCount += episodeFileCount; - acc.totalSizeOnDisk += sizeOnDisk; - - return acc; - }, - { - totalEpisodeFileCount: 0, - totalSizeOnDisk: 0, - } - ); - }, [series]); - return ( {translate('DeleteSelectedSeries')} @@ -145,45 +117,13 @@ function DeleteSeriesModalContent({ })}
-
    - {series.map(({ title, path, statistics = {} }) => { - const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; - - return ( -
  • - {title} - - {deleteFiles && ( - - - -{path} - - - {!!episodeFileCount && ( - - ( - {translate('DeleteSeriesFolderEpisodeCount', { - episodeFileCount, - size: formatBytes(sizeOnDisk), - })} - ) - - )} - - )} -
  • - ); - })} -
- - {deleteFiles && !!totalEpisodeFileCount ? ( -
- {translate('DeleteSeriesFolderEpisodeCount', { - episodeFileCount: totalEpisodeFileCount, - size: formatBytes(totalSizeOnDisk), - })} -
- ) : null} + diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx new file mode 100644 index 000000000..2cac5b3bd --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModal.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DeleteSeriesModalContent, { + DeleteSeriesFilesModalContentProps, +} from './DeleteSeriesFilesModalContent'; + +interface DeleteSeriesFilesModalProps + extends DeleteSeriesFilesModalContentProps { + isOpen: boolean; +} + +function DeleteSeriesFilesModal(props: DeleteSeriesFilesModalProps) { + const { isOpen, onModalClose } = props; + + return ( + + + + ); +} + +export default DeleteSeriesFilesModal; diff --git a/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css new file mode 100644 index 000000000..7e9ce40cb --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css @@ -0,0 +1,23 @@ +.message { + margin-bottom: 10px; +} + +.pathContainer { + margin-left: 5px; +} + +.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/Files/DeleteSeriesFilesModalContent.css.d.ts b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts new file mode 100644 index 000000000..ca4650422 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.css.d.ts @@ -0,0 +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/Files/DeleteSeriesFilesModalContent.tsx b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx new file mode 100644 index 000000000..a6c227fe3 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/Files/DeleteSeriesFilesModalContent.tsx @@ -0,0 +1,66 @@ +import React, { useCallback } from 'react'; +import CommandNames from 'Commands/CommandNames'; +import { useExecuteCommand } from 'Commands/useCommands'; +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 { kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import SeriesDeleteList from '../SeriesDeleteList'; +import useSelectedSeriesStats from '../useSelectedSeriesStats'; +import styles from './DeleteSeriesFilesModalContent.css'; + +export interface DeleteSeriesFilesModalContentProps { + onModalClose(): void; +} + +function DeleteSeriesFilesModalContent({ + onModalClose, +}: DeleteSeriesFilesModalContentProps) { + const { series, seriesIds, totalEpisodeFileCount, totalSizeOnDisk } = + useSelectedSeriesStats(); + const executeCommand = useExecuteCommand(); + + const onDeleteSeriesConfirmed = useCallback(() => { + executeCommand({ + name: CommandNames.DeleteSeriesFiles, + seriesIds, + }); + + onModalClose(); + }, [seriesIds, executeCommand, onModalClose]); + + return ( + + {translate('DeleteSelectedSeriesFiles')} + + +
+ {translate('DeleteSeriesFilesConfirmation', { + count: series.length, + })} +
+ + +
+ + + + + + +
+ ); +} + +export default DeleteSeriesFilesModalContent; diff --git a/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx b/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx new file mode 100644 index 000000000..15c8cca47 --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/SeriesDeleteList.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import Series from 'Series/Series'; +import formatBytes from 'Utilities/Number/formatBytes'; +import translate from 'Utilities/String/translate'; + +interface SeriesDeleteListStyles { + pathContainer: string; + path: string; + statistics: string; + deleteFilesMessage: string; +} + +interface SeriesDeleteListProps { + series: Series[]; + showFileDetails: boolean; + totalEpisodeFileCount: number; + totalSizeOnDisk: number; + styles: SeriesDeleteListStyles; +} + +function SeriesDeleteList({ + series, + showFileDetails, + totalEpisodeFileCount, + totalSizeOnDisk, + styles, +}: SeriesDeleteListProps) { + return ( + <> +
    + {series.map(({ title, path, statistics = {} }) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; + + return ( +
  • + {title} + + {showFileDetails ? ( + + + -{path} + + + {episodeFileCount ? ( + + ( + {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount, + size: formatBytes(sizeOnDisk), + })} + ) + + ) : null} + + ) : null} +
  • + ); + })} +
+ + {showFileDetails && totalEpisodeFileCount ? ( +
+ {translate('DeleteSeriesFolderEpisodeCount', { + episodeFileCount: totalEpisodeFileCount, + size: formatBytes(totalSizeOnDisk), + })} +
+ ) : null} + + ); +} + +export default SeriesDeleteList; diff --git a/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts b/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts new file mode 100644 index 000000000..0748cbdcd --- /dev/null +++ b/frontend/src/Series/Index/Select/Delete/useSelectedSeriesStats.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; +import Series from 'Series/Series'; +import useSeries from 'Series/useSeries'; +import sortByProp from 'Utilities/Array/sortByProp'; + +function useSelectedSeriesStats() { + const { data: allSeries } = useSeries(); + const { useSelectedIds } = useSelect(); + const seriesIds = useSelectedIds(); + + const series = useMemo((): Series[] => { + const seriesList = seriesIds.map((id) => { + return allSeries.find((s) => s.id === id); + }) as Series[]; + + return seriesList.sort(sortByProp('sortTitle')); + }, [allSeries, seriesIds]); + + const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => { + return series.reduce( + (acc, { statistics = {} }) => { + const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics; + + acc.totalEpisodeFileCount += episodeFileCount; + acc.totalSizeOnDisk += sizeOnDisk; + + return acc; + }, + { + totalEpisodeFileCount: 0, + totalSizeOnDisk: 0, + } + ); + }, [series]); + + return { + series, + seriesIds, + totalEpisodeFileCount, + totalSizeOnDisk, + }; +} + +export default useSelectedSeriesStats; diff --git a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx index 0d3cc20f2..df6a1aede 100644 --- a/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx +++ b/frontend/src/Series/Index/Select/SeriesIndexSelectFooter.tsx @@ -14,6 +14,7 @@ import { } from 'Series/useSeries'; import translate from 'Utilities/String/translate'; import DeleteSeriesModal from './Delete/DeleteSeriesModal'; +import DeleteSeriesFilesModal from './Delete/Files/DeleteSeriesFilesModal'; import EditSeriesModal from './Edit/EditSeriesModal'; import OrganizeSeriesModal from './Organize/OrganizeSeriesModal'; import ChangeMonitoringModal from './SeasonPass/ChangeMonitoringModal'; @@ -34,6 +35,9 @@ function SeriesIndexSelectFooter() { const { updateSeriesMonitor, isUpdatingSeriesMonitor } = useUpdateSeriesMonitor(); const { isBulkDeleting, bulkDeleteError } = useBulkDeleteSeries(); + const isDeleteFilesCommandExecuting = useCommandExecuting( + CommandNames.DeleteSeriesFiles + ); const isOrganizingSeries = useCommandExecuting(CommandNames.RenameSeries); @@ -46,6 +50,7 @@ function SeriesIndexSelectFooter() { const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteFilesModalOpen, setIsDeleteFilesModalOpen] = useState(false); const [isSavingSeries, setIsSavingSeries] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); const [isSavingMonitoring, setIsSavingMonitoring] = useState(false); @@ -132,6 +137,14 @@ function SeriesIndexSelectFooter() { setIsDeleteModalOpen(false); }, []); + const onDeleteFilesPress = useCallback(() => { + setIsDeleteFilesModalOpen(true); + }, []); + + const onDeleteFilesModalClose = useCallback(() => { + setIsDeleteFilesModalOpen(false); + }, []); + useEffect(() => { if (!isSaving) { setIsSavingSeries(false); @@ -195,6 +208,15 @@ function SeriesIndexSelectFooter() { > {translate('Delete')} + + + {translate('DeleteFiles')} + @@ -229,6 +251,11 @@ function SeriesIndexSelectFooter() { isOpen={isDeleteModalOpen} onModalClose={onDeleteModalClose} /> + + ); } diff --git a/frontend/src/Series/Index/SeriesIndex.tsx b/frontend/src/Series/Index/SeriesIndex.tsx index ea4ab3f76..083c438c4 100644 --- a/frontend/src/Series/Index/SeriesIndex.tsx +++ b/frontend/src/Series/Index/SeriesIndex.tsx @@ -14,7 +14,6 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; -import withScrollPosition from 'Components/withScrollPosition'; import { useCustomFiltersList } from 'Filters/useCustomFilters'; import { align, icons, kinds } from 'Helpers/Props'; import { DESCENDING } from 'Helpers/Props/sortDirections'; @@ -27,7 +26,6 @@ import { useSeriesOptions, } from 'Series/seriesOptionsStore'; import { FILTERS, useSeriesIndex } from 'Series/useSeries'; -import scrollPositions from 'Store/scrollPositions'; import { TableOptionsChangePayload } from 'typings/Table'; import translate from 'Utilities/String/translate'; import SeriesIndexFilterMenu from './Menus/SeriesIndexFilterMenu'; @@ -60,11 +58,7 @@ function getViewComponent(view: string) { return SeriesIndexTable; } -interface SeriesIndexProps { - initialScrollTop?: number; -} - -const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { +function SeriesIndex() { const { isLoading: isFetching, isFetched, @@ -148,13 +142,9 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { [setJumpToCharacter] ); - const onScroll = useCallback( - ({ scrollTop }: { scrollTop: number }) => { - setJumpToCharacter(undefined); - scrollPositions.seriesIndex = scrollTop; - }, - [setJumpToCharacter] - ); + const onScroll = useCallback(() => { + setJumpToCharacter(undefined); + }, [setJumpToCharacter]); const jumpBarItems: PageJumpBarItems = useMemo(() => { // Reset if not sorting by sortTitle @@ -296,7 +286,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore innerClassName={styles[`${view}InnerContentBody`]} - initialScrollTop={props.initialScrollTop} + scrollPositionKey="seriesIndex" onScroll={onScroll} > {isFetching && !isFetched ? : null} @@ -353,6 +343,6 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { ); -}, 'seriesIndex'); +} export default SeriesIndex; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css b/frontend/src/Series/Index/Table/SeriesIndexRow.css index 9981dd354..414f536bc 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.css +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css @@ -66,6 +66,7 @@ flex: 2 0 90px; } +.originalCountry, .originalLanguage, .qualityProfileId { composes: cell; @@ -74,6 +75,7 @@ } .releaseGroups, +.releaseTypes, .nextAiring, .previousAiring, .added, @@ -83,6 +85,12 @@ flex: 0 0 180px; } +.episodeFileQualities { + composes: cell; + + flex: 0 0 220px; +} + .seasonCount, .certification { composes: cell; @@ -130,6 +138,12 @@ flex: 0 0 120px; } +.averageSizePerEpisode { + composes: cell; + + flex: 0 0 160px; +} + .ratings { composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts index e07bb3cac..eb9fbee78 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'actions': string; 'added': string; + 'averageSizePerEpisode': string; 'banner': string; 'bannerGrow': string; 'bannerImage': string; @@ -10,6 +11,7 @@ interface CssExports { 'certification': string; 'checkInput': string; 'episodeCount': string; + 'episodeFileQualities': string; 'episodeProgress': string; 'genres': string; 'latestSeason': string; @@ -17,6 +19,7 @@ interface CssExports { 'monitorNewItems': string; 'network': string; 'nextAiring': string; + 'originalCountry': string; 'originalLanguage': string; 'overlayTitle': string; 'path': string; @@ -24,6 +27,7 @@ interface CssExports { 'qualityProfileId': string; 'ratings': string; 'releaseGroups': string; + 'releaseTypes': string; 'seasonCount': string; 'seasonFolder': string; 'seriesType': string; diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx index 48d0392ca..50faea08a 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx @@ -13,7 +13,9 @@ 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'; +import getReleaseTypeName from 'Episode/getReleaseTypeName'; import { icons } from 'Helpers/Props'; +import useCountryName from 'Internationalization/useCountryName'; import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal'; import EditSeriesModal from 'Series/Edit/EditSeriesModal'; import { Statistics } from 'Series/Series'; @@ -56,6 +58,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false); const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false); const { getIsSelected, toggleSelected } = useSelect(); + const originalCountryName = useCountryName(series?.originalCountry); const onRefreshPress = useCallback(() => { executeCommand({ @@ -147,6 +150,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { totalEpisodeCount = 0, sizeOnDisk = 0, releaseGroups = [], + releaseTypes = [], + episodeFileQualities = [], } = statistics; return ( @@ -230,6 +235,14 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { ); } + if (name === 'originalCountry') { + return ( + + {originalCountryName} + + ); + } + if (name === 'originalLanguage') { return ( @@ -386,6 +399,17 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { ); } + if (name === 'averageSizePerEpisode') { + const averageSize = + totalEpisodeCount > 0 ? sizeOnDisk / totalEpisodeCount : 0; + + return ( + + {averageSize ? formatBytes(averageSize) : null} + + ); + } + if (name === 'genres') { const joinedGenres = genres.join(', '); @@ -426,6 +450,44 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { ); } + if (name === 'releaseTypes') { + const joinedReleaseTypes = releaseTypes + .map(getReleaseTypeName) + .join(', '); + const truncatedReleaseTypes = + releaseTypes.length > 3 + ? `${releaseTypes + .slice(0, 3) + .map(getReleaseTypeName) + .join(', ')}...` + : joinedReleaseTypes; + + return ( + + {truncatedReleaseTypes} + + ); + } + + if (name === 'episodeFileQualities') { + const joinedQualities = episodeFileQualities + .map((q) => q.name) + .join(', '); + const truncatedQualities = + episodeFileQualities.length > 3 + ? `${episodeFileQualities + .slice(0, 3) + .map((q) => q.name) + .join(', ')}...` + : joinedQualities; + + return ( + + {truncatedQualities} + + ); + } + if (name === 'tags') { return ( @@ -480,6 +542,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) { diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css index 8e3b8f751..a624b1ff4 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css @@ -30,6 +30,7 @@ flex: 2 0 90px; } +.originalCountry, .originalLanguage, .qualityProfileId { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; @@ -38,6 +39,7 @@ } .releaseGroups, +.releaseTypes, .nextAiring, .previousAiring, .added, @@ -47,6 +49,12 @@ flex: 0 0 180px; } +.episodeFileQualities { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 220px; +} + .seasonCount, .certification { composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; @@ -91,6 +99,12 @@ flex: 0 0 120px; } +.averageSizePerEpisode { + composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 160px; +} + .ratings { 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 5cff4a8ec..8c1ff9e03 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts @@ -3,22 +3,26 @@ interface CssExports { 'actions': string; 'added': string; + 'averageSizePerEpisode': string; 'banner': string; 'bannerGrow': string; 'certification': string; 'episodeCount': string; + 'episodeFileQualities': string; 'episodeProgress': string; 'genres': string; 'latestSeason': string; 'monitorNewItems': string; 'network': string; 'nextAiring': string; + 'originalCountry': string; 'originalLanguage': string; 'path': string; 'previousAiring': string; 'qualityProfileId': string; 'ratings': string; 'releaseGroups': string; + 'releaseTypes': string; 'seasonCount': string; 'seasonFolder': string; 'seriesType': string; diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx index fcf80c73e..63b9dcf0e 100644 --- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx +++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.tsx @@ -16,6 +16,7 @@ import { } from 'Series/seriesOptionsStore'; import { CheckInputChanged } from 'typings/inputs'; import { TableOptionsChangePayload } from 'typings/Table'; +import translate from 'Utilities/String/translate'; import hasGrowableColumns from './hasGrowableColumns'; import SeriesIndexTableOptions from './SeriesIndexTableOptions'; import styles from './SeriesIndexTableHeader.css'; @@ -95,7 +96,10 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) { optionsComponent={SeriesIndexTableOptions} onTableOptionChange={onTableOptionChange} > - + ); diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index d3d933ac8..107614411 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -1,5 +1,7 @@ import ModelBase from 'App/ModelBase'; +import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; +import Quality from 'Quality/Quality'; export type SeriesType = 'anime' | 'daily' | 'standard'; export type SeriesMonitor = @@ -34,10 +36,11 @@ export interface Statistics { percentOfEpisodes: number; previousAiring?: Date; releaseGroups: string[]; + releaseTypes: ReleaseType[]; + episodeFileQualities: Quality[]; sizeOnDisk: number; totalEpisodeCount: number; monitoredEpisodeCount: number; - lastAired?: string; } export interface Season { @@ -71,13 +74,15 @@ interface Series extends ModelBase { certification: string; cleanTitle: string; ended: boolean; - firstAired: string; + firstAired?: string; + lastAired?: string; genres: string[]; images: Image[]; imdbId?: string; monitored: boolean; monitorNewItems: MonitorNewItems; network: string; + originalCountry: string; originalLanguage: Language; overview: string; path: string; diff --git a/frontend/src/Series/seriesOptionsStore.ts b/frontend/src/Series/seriesOptionsStore.ts index ba1bb44a4..a833e9b5d 100644 --- a/frontend/src/Series/seriesOptionsStore.ts +++ b/frontend/src/Series/seriesOptionsStore.ts @@ -125,6 +125,12 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } = isSortable: true, isVisible: false, }, + { + name: 'originalCountry', + label: () => translate('OriginalCountry'), + isSortable: true, + isVisible: false, + }, { name: 'originalLanguage', label: () => translate('OriginalLanguage'), @@ -185,6 +191,12 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } = isSortable: true, isVisible: false, }, + { + name: 'averageSizePerEpisode', + label: () => translate('AverageSize'), + isSortable: true, + isVisible: false, + }, { name: 'genres', label: () => translate('Genres'), @@ -209,6 +221,18 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } = isSortable: false, isVisible: false, }, + { + name: 'releaseTypes', + label: () => translate('ReleaseTypes'), + isSortable: false, + isVisible: false, + }, + { + name: 'episodeFileQualities', + label: () => translate('EpisodeFileQualities'), + isSortable: false, + isVisible: false, + }, { name: 'tags', label: () => translate('Tags'), diff --git a/frontend/src/Series/useSeries.ts b/frontend/src/Series/useSeries.ts index 63497121d..6d1bff479 100644 --- a/frontend/src/Series/useSeries.ts +++ b/frontend/src/Series/useSeries.ts @@ -113,6 +113,14 @@ const SORT_PREDICATES = { return item.statistics?.sizeOnDisk ?? 0; }, + averageSizePerEpisode: (item: Series, _direction: SortDirection) => { + const totalEpisodeCount = item.statistics?.totalEpisodeCount ?? 0; + + return totalEpisodeCount > 0 + ? (item.statistics?.sizeOnDisk ?? 0) / totalEpisodeCount + : 0; + }, + network: (item: Series, _direction: SortDirection) => { const network = item.network; @@ -246,6 +254,24 @@ const FILTER_PREDICATES = { return predicate(releaseGroups, filterValue); }, + releaseTypes: (item: Series, filterValue: string[], type: FilterType) => { + const releaseTypes = item.statistics?.releaseTypes ?? []; + const predicate = getFilterTypePredicate(type); + return predicate(releaseTypes, filterValue); + }, + + episodeFileQualities: ( + item: Series, + filterValue: number[], + type: FilterType + ) => { + const episodeFileQualities = ( + item.statistics?.episodeFileQualities ?? [] + ).map((q) => q.id); + const predicate = getFilterTypePredicate(type); + return predicate(episodeFileQualities, filterValue); + }, + seasonCount: (item: Series, filterValue: number, type: FilterType) => { const predicate = getFilterTypePredicate(type); const seasonCount = item.statistics?.seasonCount ?? 0; @@ -258,6 +284,20 @@ const FILTER_PREDICATES = { return predicate(sizeOnDisk, filterValue); }, + averageSizePerEpisode: ( + item: Series, + filterValue: number, + type: FilterType + ) => { + const predicate = getFilterTypePredicate(type); + const totalEpisodeCount = item.statistics?.totalEpisodeCount ?? 0; + const averageSize = + totalEpisodeCount > 0 + ? (item.statistics?.sizeOnDisk ?? 0) / totalEpisodeCount + : 0; + return predicate(averageSize, filterValue); + }, + hasMissingSeason: (item: Series, filterValue: boolean, type: FilterType) => { const predicate = getFilterTypePredicate(type); const seasons = item.seasons ?? []; @@ -444,6 +484,12 @@ export const FILTER_BUILDER: FilterBuilderProp[] = [ type: filterBuilderTypes.NUMBER, valueType: filterBuilderValueTypes.BYTES, }, + { + name: 'averageSizePerEpisode', + label: () => translate('AverageSizePerEpisode'), + type: filterBuilderTypes.NUMBER, + valueType: filterBuilderValueTypes.BYTES, + }, { name: 'genres', label: () => translate('Genres'), @@ -493,6 +539,18 @@ export const FILTER_BUILDER: FilterBuilderProp[] = [ label: () => translate('ReleaseGroups'), type: filterBuilderTypes.ARRAY, }, + { + name: 'releaseTypes', + label: () => translate('ReleaseTypes'), + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.RELEASE_TYPES, + }, + { + name: 'episodeFileQualities', + label: () => translate('EpisodeFileQualities'), + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.QUALITY, + }, { name: 'ratings', label: () => translate('Rating'), diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx index 335a7a555..922951280 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx @@ -83,6 +83,7 @@ function CustomFormat({ @@ -90,6 +91,7 @@ function CustomFormat({ diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx index cfcd461a9..03891dd69 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx @@ -94,6 +94,7 @@ function ManageCustomFormatsModalRow({ diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx index afb91aa97..5d05ccafd 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx @@ -73,6 +73,7 @@ function Specification({ diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.tsx index d57106a96..ddb2efd0f 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import Card from 'Components/Card'; import Label from 'Components/Label'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { kinds } from 'Helpers/Props'; import { deleteDownloadClient } from 'Store/Actions/settingsActions'; import { useTagList } from 'Tags/useTags'; @@ -14,6 +16,7 @@ import styles from './DownloadClient.css'; interface DownloadClientProps { id: number; name: string; + protocol: DownloadProtocol; enable: boolean; priority: number; tags: number[]; @@ -22,6 +25,7 @@ interface DownloadClientProps { function DownloadClient({ id, name, + protocol, enable, priority, tags, @@ -65,6 +69,8 @@ function DownloadClient({
{name}
+ + {enable ? ( ) : ( diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index abcf9fd87..bf1450375 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -37,6 +37,12 @@ const COLUMNS: Column[] = [ isSortable: true, isVisible: true, }, + { + name: 'protocol', + label: () => translate('Protocol'), + isSortable: true, + isVisible: true, + }, { name: 'implementation', label: () => translate('Implementation'), diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css index 242e0c84e..71736b66f 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css @@ -1,4 +1,5 @@ .name, +.protocol, .enable, .tags, .priority, @@ -8,4 +9,4 @@ composes: cell from '~Components/Table/Cells/TableRowCell.css'; word-break: break-all; -} \ No newline at end of file +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts index 74553b4f9..c72af477c 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.css.d.ts @@ -5,6 +5,7 @@ interface CssExports { 'implementation': string; 'name': string; 'priority': string; + 'protocol': string; 'removeCompletedDownloads': string; 'removeFailedDownloads': string; 'tags': string; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx index 5cb755bc6..9bff2120b 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import { useSelect } from 'App/Select/SelectContext'; import Label from 'Components/Label'; import SeriesTagList from 'Components/SeriesTagList'; @@ -6,6 +7,7 @@ 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 DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { kinds } from 'Helpers/Props'; import DownloadClient from 'typings/DownloadClient'; import { SelectStateInputProps } from 'typings/props'; @@ -15,6 +17,7 @@ import styles from './ManageDownloadClientsModalRow.css'; interface ManageDownloadClientsModalRowProps { id: number; name: string; + protocol: DownloadProtocol; enable: boolean; priority: number; removeCompletedDownloads: boolean; @@ -30,6 +33,7 @@ function ManageDownloadClientsModalRow( const { id, name, + protocol, enable, priority, removeCompletedDownloads, @@ -62,6 +66,10 @@ function ManageDownloadClientsModalRow( {name} + + + + {implementation} diff --git a/frontend/src/Settings/General/AnalyticSettings.tsx b/frontend/src/Settings/General/AnalyticSettings.tsx index 79892bb67..1defb9970 100644 --- a/frontend/src/Settings/General/AnalyticSettings.tsx +++ b/frontend/src/Settings/General/AnalyticSettings.tsx @@ -6,11 +6,11 @@ import FormLabel from 'Components/Form/FormLabel'; import { inputTypes, sizes } from 'Helpers/Props'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; interface AnalyticSettingsProps { - analyticsEnabled: PendingSection['analyticsEnabled']; + analyticsEnabled: PendingSection['analyticsEnabled']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/BackupSettings.tsx b/frontend/src/Settings/General/BackupSettings.tsx index 83d398373..9954a9d40 100644 --- a/frontend/src/Settings/General/BackupSettings.tsx +++ b/frontend/src/Settings/General/BackupSettings.tsx @@ -7,13 +7,13 @@ import { inputTypes } from 'Helpers/Props'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; interface BackupSettingsProps { - backupFolder: PendingSection['backupFolder']; - backupInterval: PendingSection['backupInterval']; - backupRetention: PendingSection['backupRetention']; + backupFolder: PendingSection['backupFolder']; + backupInterval: PendingSection['backupInterval']; + backupRetention: PendingSection['backupRetention']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/GeneralSettings.tsx b/frontend/src/Settings/General/GeneralSettings.tsx index 8f8d6baae..7cab9a1b1 100644 --- a/frontend/src/Settings/General/GeneralSettings.tsx +++ b/frontend/src/Settings/General/GeneralSettings.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import CommandNames from 'Commands/CommandNames'; import { useCommandExecuting } from 'Commands/useCommands'; import Alert from 'Components/Alert'; @@ -11,13 +10,6 @@ import PageContentBody from 'Components/Page/PageContentBody'; import usePrevious from 'Helpers/Hooks/usePrevious'; import { kinds } from 'Helpers/Props'; import SettingsToolbar from 'Settings/SettingsToolbar'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { - fetchGeneralSettings, - saveGeneralSettings, - setGeneralSettingsValue, -} from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { useIsWindowsService } from 'System/Status/useSystemStatus'; import { useRestart } from 'System/useSystem'; import { InputChanged } from 'typings/inputs'; @@ -29,8 +21,7 @@ import LoggingSettings from './LoggingSettings'; import ProxySettings from './ProxySettings'; import SecuritySettings from './SecuritySettings'; import UpdateSettings from './UpdateSettings'; - -const SECTION = 'general'; +import { useManageGeneralSettings } from './useGeneralSettings'; const requiresRestartKeys = [ 'bindAddress', @@ -44,24 +35,24 @@ const requiresRestartKeys = [ ]; function GeneralSettings() { - const dispatch = useDispatch(); const isWindowsService = useIsWindowsService(); const { mutate: restart } = useRestart(); const isResettingApiKey = useCommandExecuting(CommandNames.ResetApiKey); const { - isFetching, - isPopulated, - isSaving, - error, - saveError, settings, - hasSettings, + isFetching, + isFetched, + error, + updateSetting, + saveSettings, + isSaving, + saveError, hasPendingChanges, pendingChanges, validationErrors, validationWarnings, - } = useSelector(createSettingsSectionSelector(SECTION)); + } = useManageGeneralSettings(); const wasResettingApiKey = usePrevious(isResettingApiKey); const wasSaving = usePrevious(isSaving); @@ -72,15 +63,15 @@ function GeneralSettings() { const handleInputChange = useCallback( (change: InputChanged) => { - // @ts-expect-error - actions aren't typed - dispatch(setGeneralSettingsValue(change)); + // @ts-expect-error input change events aren't typed + updateSetting(change.name, change.value); }, - [dispatch] + [updateSetting] ); const handleSavePress = useCallback(() => { - dispatch(saveGeneralSettings()); - }, [dispatch]); + saveSettings(); + }, [saveSettings]); const handleConfirmRestart = useCallback(() => { setIsRestartRequiredModalOpen(false); @@ -91,20 +82,6 @@ function GeneralSettings() { setIsRestartRequiredModalOpen(false); }, []); - useEffect(() => { - dispatch(fetchGeneralSettings()); - - return () => { - dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); - }; - }, [dispatch]); - - useEffect(() => { - if (!isResettingApiKey && wasResettingApiKey) { - dispatch(fetchGeneralSettings()); - } - }, [isResettingApiKey, wasResettingApiKey, dispatch]); - useEffect(() => { const isRestartedRequired = previousPendingChanges && @@ -132,7 +109,7 @@ function GeneralSettings() { /> - {isFetching && !isPopulated ? : null} + {isFetching && !isFetched ? : null} {!isFetching && error ? ( @@ -140,7 +117,7 @@ function GeneralSettings() { ) : null} - {hasSettings && isPopulated && !error ? ( + {settings && isFetched && !error ? (
['bindAddress']; - port: PendingSection['port']; - urlBase: PendingSection['urlBase']; - instanceName: PendingSection['instanceName']; - applicationUrl: PendingSection['applicationUrl']; - enableSsl: PendingSection['enableSsl']; - sslPort: PendingSection['sslPort']; - sslKeyPath: PendingSection['sslKeyPath']; - sslCertPath: PendingSection['sslCertPath']; - sslCertPassword: PendingSection['sslCertPassword']; - launchBrowser: PendingSection['launchBrowser']; + bindAddress: PendingSection['bindAddress']; + port: PendingSection['port']; + urlBase: PendingSection['urlBase']; + instanceName: PendingSection['instanceName']; + applicationUrl: PendingSection['applicationUrl']; + enableSsl: PendingSection['enableSsl']; + sslPort: PendingSection['sslPort']; + sslKeyPath: PendingSection['sslKeyPath']; + sslCertPath: PendingSection['sslCertPath']; + sslCertPassword: PendingSection['sslCertPassword']; + launchBrowser: PendingSection['launchBrowser']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/LoggingSettings.tsx b/frontend/src/Settings/General/LoggingSettings.tsx index 78ddefb70..523717711 100644 --- a/frontend/src/Settings/General/LoggingSettings.tsx +++ b/frontend/src/Settings/General/LoggingSettings.tsx @@ -8,8 +8,8 @@ import { inputTypes } from 'Helpers/Props'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; const logLevelOptions: EnhancedSelectInputValue[] = [ { @@ -33,8 +33,8 @@ const logLevelOptions: EnhancedSelectInputValue[] = [ ]; interface LoggingSettingsProps { - logLevel: PendingSection['logLevel']; - logSizeLimit: PendingSection['logSizeLimit']; + logLevel: PendingSection['logLevel']; + logSizeLimit: PendingSection['logSizeLimit']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/ProxySettings.tsx b/frontend/src/Settings/General/ProxySettings.tsx index 2595c8dc2..848c337e8 100644 --- a/frontend/src/Settings/General/ProxySettings.tsx +++ b/frontend/src/Settings/General/ProxySettings.tsx @@ -7,18 +7,18 @@ import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectI import { inputTypes, sizes } from 'Helpers/Props'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; interface ProxySettingsProps { - proxyEnabled: PendingSection['proxyEnabled']; - proxyType: PendingSection['proxyType']; - proxyHostname: PendingSection['proxyHostname']; - proxyPort: PendingSection['proxyPort']; - proxyUsername: PendingSection['proxyUsername']; - proxyPassword: PendingSection['proxyPassword']; - proxyBypassFilter: PendingSection['proxyBypassFilter']; - proxyBypassLocalAddresses: PendingSection['proxyBypassLocalAddresses']; + proxyEnabled: PendingSection['proxyEnabled']; + proxyType: PendingSection['proxyType']; + proxyHostname: PendingSection['proxyHostname']; + proxyPort: PendingSection['proxyPort']; + proxyUsername: PendingSection['proxyUsername']; + proxyPassword: PendingSection['proxyPassword']; + proxyBypassFilter: PendingSection['proxyBypassFilter']; + proxyBypassLocalAddresses: PendingSection['proxyBypassLocalAddresses']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/SecuritySettings.tsx b/frontend/src/Settings/General/SecuritySettings.tsx index 0f339e0a3..2bcd0ad0b 100644 --- a/frontend/src/Settings/General/SecuritySettings.tsx +++ b/frontend/src/Settings/General/SecuritySettings.tsx @@ -13,8 +13,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import { icons, inputTypes, kinds } from 'Helpers/Props'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; export const authenticationMethodOptions: EnhancedSelectInputValue[] = [ { @@ -85,13 +85,13 @@ const certificateValidationOptions: EnhancedSelectInputValue[] = [ ]; interface SecuritySettingsProps { - authenticationMethod: PendingSection['authenticationMethod']; - authenticationRequired: PendingSection['authenticationRequired']; - username: PendingSection['username']; - password: PendingSection['password']; - passwordConfirmation: PendingSection['passwordConfirmation']; - apiKey: PendingSection['apiKey']; - certificateValidation: PendingSection['certificateValidation']; + authenticationMethod: PendingSection['authenticationMethod']; + authenticationRequired: PendingSection['authenticationRequired']; + username: PendingSection['username']; + password: PendingSection['password']; + passwordConfirmation: PendingSection['passwordConfirmation']; + apiKey: PendingSection['apiKey']; + certificateValidation: PendingSection['certificateValidation']; isResettingApiKey: boolean; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/Settings/General/UpdateSettings.tsx b/frontend/src/Settings/General/UpdateSettings.tsx index 0861ef92d..df8d7ffed 100644 --- a/frontend/src/Settings/General/UpdateSettings.tsx +++ b/frontend/src/Settings/General/UpdateSettings.tsx @@ -9,17 +9,17 @@ import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; import { useSystemStatusData } from 'System/Status/useSystemStatus'; import { InputChanged } from 'typings/inputs'; import { PendingSection } from 'typings/pending'; -import General from 'typings/Settings/General'; import titleCase from 'Utilities/String/titleCase'; import translate from 'Utilities/String/translate'; +import { GeneralSettingsModel } from './useGeneralSettings'; const branchValues = ['main', 'develop']; interface UpdateSettingsProps { - branch: PendingSection['branch']; - updateAutomatically: PendingSection['updateAutomatically']; - updateMechanism: PendingSection['updateMechanism']; - updateScriptPath: PendingSection['updateScriptPath']; + branch: PendingSection['branch']; + updateAutomatically: PendingSection['updateAutomatically']; + updateMechanism: PendingSection['updateMechanism']; + updateScriptPath: PendingSection['updateScriptPath']; onInputChange: (change: InputChanged) => void; } diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/Settings/General/useGeneralSettings.ts similarity index 68% rename from frontend/src/typings/Settings/General.ts rename to frontend/src/Settings/General/useGeneralSettings.ts index 339485f00..31cc2a039 100644 --- a/frontend/src/typings/Settings/General.ts +++ b/frontend/src/Settings/General/useGeneralSettings.ts @@ -1,3 +1,9 @@ +import { + useManageSettings, + useSaveSettings, + useSettings, +} from 'Settings/useSettings'; + export type UpdateMechanism = | 'builtIn' | 'script' @@ -5,7 +11,7 @@ export type UpdateMechanism = | 'apt' | 'docker'; -export default interface General { +export interface GeneralSettingsModel { bindAddress: string; port: number; sslPort: number; @@ -45,3 +51,17 @@ export default interface General { backupRetention: number; id: number; } + +const PATH = '/settings/general'; + +export const useGeneralSettings = () => { + return useSettings(PATH); +}; + +export const useManageGeneralSettings = () => { + return useManageSettings(PATH); +}; + +export const useSaveGeneralSettings = () => { + return useSaveSettings(PATH); +}; diff --git a/frontend/src/Settings/General/useUpdateSettings.ts b/frontend/src/Settings/General/useUpdateSettings.ts index d2e648e2c..765aa0ab7 100644 --- a/frontend/src/Settings/General/useUpdateSettings.ts +++ b/frontend/src/Settings/General/useUpdateSettings.ts @@ -1,5 +1,5 @@ import useApiQuery from 'Helpers/Hooks/useApiQuery'; -import { UpdateMechanism } from 'typings/Settings/General'; +import { UpdateMechanism } from './useGeneralSettings'; interface UpdateSettings { branch: string; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx index 7f5feafab..4b2cbb213 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -1,12 +1,12 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; 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; + title?: string; + tvdbId?: number; isOpen: boolean; onModalClose: () => void; onDeleteImportListExclusionPress?: () => void; @@ -17,22 +17,11 @@ function EditImportListExclusionModal( ) { const { isOpen, onModalClose, ...otherProps } = props; - const dispatch = useDispatch(); - - const handleModalClose = useCallback(() => { - dispatch( - clearPendingChanges({ - section: 'settings.importListExclusions', - }) - ); - onModalClose(); - }, [dispatch, onModalClose]); - return ( - + ); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css index 97e132552..a2b6014df 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css @@ -1,9 +1,3 @@ -.body { - composes: modalBody from '~Components/Modal/ModalBody.css'; - - flex: 1 1 430px; -} - .deleteButton { composes: button from '~Components/Link/Button.css'; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css.d.ts index 7881f9867..c5f0ef8a7 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css.d.ts @@ -1,7 +1,6 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'body': string; 'deleteButton': string; } export const cssExports: CssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index 31e92005d..e161083c2 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -1,115 +1,70 @@ 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 { InputChanged } from 'typings/inputs'; -import { PendingSection } from 'typings/pending'; import translate from 'Utilities/String/translate'; +import { useManageImportListExclusion } from './useImportListExclusions'; import styles from './EditImportListExclusionModalContent.css'; -const newImportListExclusion = { - title: '', - tvdbId: 0, -}; - -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 { - isFetching, - error, - isSaving, - saveError, - item: settings.settings as PendingSection, - ...settings, - }; - } - ); -} - interface EditImportListExclusionModalContentProps { id?: number; + title?: string; + tvdbId?: number; onModalClose: () => void; onDeleteImportListExclusionPress?: () => void; } function EditImportListExclusionModalContent({ id, + title: existingTitle, + tvdbId: existingTvdbId, onModalClose, onDeleteImportListExclusionPress, }: EditImportListExclusionModalContentProps) { - const { isFetching, isSaving, item, error, saveError, ...otherProps } = - useSelector(createImportListExclusionSelector(id)); + const { + item, + isSaving, + saveError, + validationErrors, + validationWarnings, + updateValue, + save, + } = useManageImportListExclusion({ + id, + title: existingTitle, + tvdbId: existingTvdbId, + }); const { title, tvdbId } = item; - - const dispatch = useDispatch(); - const previousIsSaving = usePrevious(isSaving); - - const dispatchSetImportListExclusionValue = (payload: { - name: string; - value: string | number; - }) => { - // @ts-expect-error 'setImportListExclusionValue' isn't typed yet - dispatch(setImportListExclusionValue(payload)); - }; + const wasSaving = usePrevious(isSaving); useEffect(() => { - if (!id) { - Object.entries(newImportListExclusion).forEach(([name, value]) => { - dispatchSetImportListExclusionValue({ name, value }); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (previousIsSaving && !isSaving && !saveError) { + if (wasSaving && !isSaving && !saveError) { onModalClose(); } - }, [previousIsSaving, isSaving, saveError, onModalClose]); + }, [isSaving, wasSaving, saveError, onModalClose]); - const onSavePress = useCallback(() => { - dispatch(saveImportListExclusion({ id })); - }, [dispatch, id]); - - const onInputChange = useCallback( - (change: InputChanged) => { - // @ts-expect-error 'setImportListExclusionValue' isn't typed yet - dispatch(setImportListExclusionValue(change)); + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + updateValue(name, value); }, - [dispatch] + [updateValue] ); + const handleSavePress = useCallback(() => { + save(); + }, [save]); + return ( @@ -118,42 +73,35 @@ function EditImportListExclusionModalContent({ : translate('AddImportListExclusion')} - - {isFetching ? : null} + + + + {translate('Title')} - {!isFetching && error ? ( - - {translate('AddImportListExclusionError')} - - ) : null} + + - {!isFetching && !error ? ( - - - {translate('Title')} + + {translate('TvdbId')} - - - - - {translate('TvdbId')} - - - - - ) : null} + + + @@ -172,7 +120,7 @@ function EditImportListExclusionModalContent({ {translate('Save')} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx index 176a558a2..065d0cad9 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; import { useSelect } from 'App/Select/SelectContext'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; @@ -8,23 +7,30 @@ 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 { + ImportListExclusion, + useDeleteImportListExclusion, +} from './useImportListExclusions'; import styles from './ImportListExclusionRow.css'; -type ImportListExclusionRowProps = ImportListExclusion; +interface ImportListExclusionRowProps extends ImportListExclusion { + onModalClose: () => void; +} function ImportListExclusionRow({ id, tvdbId, title, + onModalClose, }: ImportListExclusionRowProps) { const { toggleSelected, useIsSelected } = useSelect(); const isSelected = useIsSelected(id); + const { deleteImportListExclusion } = useDeleteImportListExclusion(id); + const handleSelectedChange = useCallback( ({ id, value, shiftKey = false }: SelectStateInputProps) => { toggleSelected({ @@ -36,14 +42,17 @@ function ImportListExclusionRow({ [toggleSelected] ); - const dispatch = useDispatch(); - const [ isEditImportListExclusionModalOpen, setEditImportListExclusionModalOpen, setEditImportListExclusionModalClosed, ] = useModalOpenState(false); + const handleEditModalClose = useCallback(() => { + setEditImportListExclusionModalClosed(); + onModalClose(); + }, [setEditImportListExclusionModalClosed, onModalClose]); + const [ isDeleteImportListExclusionModalOpen, setDeleteImportListExclusionModalOpen, @@ -51,8 +60,8 @@ function ImportListExclusionRow({ ] = useModalOpenState(false); const handleDeletePress = useCallback(() => { - dispatch(deleteImportListExclusion({ id })); - }, [id, dispatch]); + deleteImportListExclusion(); + }, [deleteImportListExclusion]); return ( @@ -68,14 +77,17 @@ function ImportListExclusionRow({ diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index f4fd46903..37ae61cd7 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -1,8 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; import { SelectProvider, useSelect } from 'App/Select/SelectContext'; -import AppState from 'App/State/AppState'; import FieldSet from 'Components/FieldSet'; import IconButton from 'Components/Link/IconButton'; import SpinnerButton from 'Components/Link/SpinnerButton'; @@ -14,21 +11,9 @@ 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 usePrevious from 'Helpers/Hooks/usePrevious'; import { icons, kinds } from 'Helpers/Props'; import { SortDirection } from 'Helpers/Props/sortDirections'; -import { - bulkDeleteImportListExclusions, - clearImportListExclusions, - fetchImportListExclusions, - gotoImportListExclusionPage, - setImportListExclusionSort, - setImportListExclusionTableOption, -} from 'Store/Actions/Settings/importListExclusions'; -import ImportListExclusion from 'typings/ImportListExclusion'; import { CheckInputChanged } from 'typings/inputs'; import { TableOptionsChangePayload } from 'typings/Table'; import { @@ -37,7 +22,16 @@ import { } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; import EditImportListExclusionModal from './EditImportListExclusionModal'; +import { + setImportListExclusionOption, + setImportListExclusionSort, + useImportListExclusionOptions, +} from './importListExclusionOptionsStore'; import ImportListExclusionRow from './ImportListExclusionRow'; +import useImportListExclusions, { + ImportListExclusion, + useDeleteImportListExclusions, +} from './useImportListExclusions'; import styles from './ImportListExclusions.css'; const COLUMNS: Column[] = [ @@ -62,40 +56,27 @@ const COLUMNS: Column[] = [ }, ]; -function createImportListExclusionsSelector() { - return createSelector( - (state: AppState) => state.settings.importListExclusions, - (importListExclusions) => { - return { - ...importListExclusions, - }; - } - ); -} - function ImportListExclusionsContent() { - const requestCurrentPage = useCurrentPage(); - const { - isFetching, - isPopulated, - items, - pageSize, - sortKey, - error, - sortDirection, - page, + records, totalPages, totalRecords, - isDeleting, - deleteError, - } = useSelector(createImportListExclusionsSelector()); + isFetching, + isFetched, + isLoading, + error, + page, + goToPage, + refetch, + } = useImportListExclusions(); - const dispatch = useDispatch(); + const { pageSize, sortKey, sortDirection } = useImportListExclusionOptions(); + + const { deleteImportListExclusions, isDeleting } = + useDeleteImportListExclusions(); const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); - const previousIsDeleting = usePrevious(isDeleting); const { allSelected, @@ -119,100 +100,64 @@ function ImportListExclusionsContent() { const handleDeleteSelectedPress = useCallback(() => { setIsConfirmDeleteModalOpen(true); - }, [setIsConfirmDeleteModalOpen]); + }, []); const handleDeleteSelectedConfirmed = useCallback(() => { - dispatch(bulkDeleteImportListExclusions({ ids: getSelectedIds() })); + deleteImportListExclusions({ ids: getSelectedIds() }); setIsConfirmDeleteModalOpen(false); - }, [getSelectedIds, setIsConfirmDeleteModalOpen, dispatch]); + unselectAll(); + }, [getSelectedIds, deleteImportListExclusions, unselectAll]); const handleConfirmDeleteModalClose = useCallback(() => { setIsConfirmDeleteModalOpen(false); - }, [setIsConfirmDeleteModalOpen]); - - const { - handleFirstPagePress, - handlePreviousPagePress, - handleNextPagePress, - handleLastPagePress, - handlePageSelect, - } = usePaging({ - page, - totalPages, - gotoPage: gotoImportListExclusionPage, - }); + }, []); const handleSortPress = useCallback( (sortKey: string, sortDirection?: SortDirection) => { - dispatch(setImportListExclusionSort({ sortKey, sortDirection })); + setImportListExclusionSort({ sortKey, sortDirection }); }, - [dispatch] + [] ); const handleTableOptionChange = useCallback( (payload: TableOptionsChangePayload) => { - dispatch(setImportListExclusionTableOption(payload)); - if (payload.pageSize) { - dispatch(gotoImportListExclusionPage({ page: 1 })); + setImportListExclusionOption('pageSize', payload.pageSize as number); + goToPage(1); } }, - [dispatch] + [goToPage] ); - 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) { - unselectAll(); - - dispatch(fetchImportListExclusions()); - } - }, [ - previousIsDeleting, - isDeleting, - deleteError, - items, - dispatch, - unselectAll, - ]); - const [ isAddImportListExclusionModalOpen, setAddImportListExclusionModalOpen, setAddImportListExclusionModalClosed, ] = useModalOpenState(false); - const isFetchingForFirstTime = isFetching && !isPopulated; + const handleAddModalClose = useCallback(() => { + setAddImportListExclusionModalClosed(); + refetch(); + }, [setAddImportListExclusionModalClosed, refetch]); + + useEffect(() => { + const repopulate = () => { + refetch(); + }; + + registerPagePopulator(repopulate); + + return () => { + unregisterPagePopulator(repopulate); + }; + }, [refetch]); return (
{children} + {children} +
- {items.map((item) => { - return ; + {records.map((item) => { + return ( + + ); })} @@ -248,6 +199,7 @@ function ImportListExclusionsContent() { @@ -260,16 +212,12 @@ function ImportListExclusionsContent() { totalPages={totalPages} totalRecords={totalRecords} isFetching={isFetching} - onFirstPagePress={handleFirstPagePress} - onPreviousPagePress={handlePreviousPagePress} - onNextPagePress={handleNextPagePress} - onLastPagePress={handleLastPagePress} - onPageSelect={handlePageSelect} + onPageSelect={goToPage} /> + items={records}> ); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/importListExclusionOptionsStore.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/importListExclusionOptionsStore.ts new file mode 100644 index 000000000..d0c043d1f --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/importListExclusionOptionsStore.ts @@ -0,0 +1,25 @@ +import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore'; +import { SortDirection } from 'Helpers/Props/sortDirections'; + +export interface ImportListExclusionOptions { + pageSize: number; + sortKey: string; + sortDirection: SortDirection; +} + +const { useOptions, setOptions, setOption, setSort } = + createOptionsStore( + 'import_list_exclusion_options', + () => { + return { + pageSize: 20, + sortKey: 'id', + sortDirection: 'descending', + }; + } + ); + +export const useImportListExclusionOptions = useOptions; +export const setImportListExclusionOptions = setOptions; +export const setImportListExclusionOption = setOption; +export const setImportListExclusionSort = setSort; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/useImportListExclusions.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/useImportListExclusions.ts new file mode 100644 index 000000000..076677b40 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/useImportListExclusions.ts @@ -0,0 +1,163 @@ +import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; +import ModelBase from 'App/ModelBase'; +import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import usePage from 'Helpers/Hooks/usePage'; +import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery'; +import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { useImportListExclusionOptions } from './importListExclusionOptionsStore'; + +export interface ImportListExclusion extends ModelBase { + tvdbId: number; + title: string; +} + +const PATH = '/importlistexclusion'; + +const NEW_IMPORT_LIST_EXCLUSION = { + title: '', + tvdbId: 0, +}; + +interface BulkImportListExclusionData { + ids: number[]; +} + +const useImportListExclusions = () => { + const { page, goToPage } = usePage('importListExclusion'); + const { pageSize, sortKey, sortDirection } = useImportListExclusionOptions(); + + const { refetch, ...query } = usePagedApiQuery({ + path: PATH, + page, + pageSize, + sortKey, + sortDirection, + queryOptions: { + placeholderData: keepPreviousData, + }, + }); + + return { + ...query, + goToPage, + page, + refetch, + }; +}; + +export default useImportListExclusions; + +interface ManageImportListExclusionOptions { + id?: number; + title?: string; + tvdbId?: number; +} + +export const useManageImportListExclusion = ({ + id, + title, + tvdbId, +}: ManageImportListExclusionOptions) => { + const queryClient = useQueryClient(); + + const item = useMemo(() => { + return id + ? { title: title ?? '', tvdbId: tvdbId ?? 0 } + : NEW_IMPORT_LIST_EXCLUSION; + }, [id, title, tvdbId]); + + const { pendingChanges, setPendingChange } = + usePendingChangesStore({}); + + const { + mutate, + isPending: isSaving, + error: saveError, + } = useApiMutation({ + path: id ? `${PATH}/${id}` : PATH, + method: id ? 'PUT' : 'POST', + mutationOptions: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [PATH] }); + }, + }, + }); + + const { settings, validationErrors, validationWarnings } = useMemo(() => { + return selectSettings(item, pendingChanges, saveError); + }, [item, pendingChanges, saveError]); + + const updateValue = useCallback( + (name: string, value: unknown) => { + // @ts-expect-error - name is not yet typed + setPendingChange(name, value); + }, + [setPendingChange] + ); + + const save = useCallback(() => { + const payload = { + ...item, + ...pendingChanges, + } as ImportListExclusion; + + if (id) { + payload.id = id; + } + + mutate(payload); + }, [id, item, pendingChanges, mutate]); + + return { + item: settings, + isSaving, + saveError, + validationErrors, + validationWarnings, + updateValue, + save, + }; +}; + +export const useDeleteImportListExclusion = (id: number) => { + const queryClient = useQueryClient(); + + const { mutate, isPending } = useApiMutation({ + path: `${PATH}/${id}`, + method: 'DELETE', + mutationOptions: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [PATH] }); + }, + }, + }); + + return { + deleteImportListExclusion: mutate, + isDeleting: isPending, + }; +}; + +export const useDeleteImportListExclusions = () => { + const queryClient = useQueryClient(); + + const { mutate, isPending } = useApiMutation< + unknown, + BulkImportListExclusionData + >({ + path: `${PATH}/bulk`, + method: 'DELETE', + mutationOptions: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [PATH] }); + }, + }, + }); + + return { + deleteImportListExclusions: mutate, + isDeleting: isPending, + }; +}; diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx b/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx index d5ebf4fa9..6613428b5 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx @@ -76,6 +76,7 @@ function ImportList({ diff --git a/frontend/src/Settings/Indexers/IndexerSettings.tsx b/frontend/src/Settings/Indexers/IndexerSettings.tsx index c4878a8c0..908bbca95 100644 --- a/frontend/src/Settings/Indexers/IndexerSettings.tsx +++ b/frontend/src/Settings/Indexers/IndexerSettings.tsx @@ -1,13 +1,10 @@ import React, { useCallback, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import { icons } from 'Helpers/Props'; import SettingsToolbar from 'Settings/SettingsToolbar'; -import { testAllIndexers } from 'Store/Actions/settingsActions'; import { SaveCallback, SettingsStateChange, @@ -16,12 +13,10 @@ import translate from 'Utilities/String/translate'; import Indexers from './Indexers/Indexers'; import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal'; import IndexerOptions from './Options/IndexerOptions'; +import { useTestAllIndexers } from './useIndexers'; function IndexerSettings() { - const dispatch = useDispatch(); - const isTestingAll = useSelector( - (state: AppState) => state.settings.indexers.isTestingAll - ); + const { isTestingAllIndexers, testAllIndexers } = useTestAllIndexers(); const saveOptions = useRef<() => void>(); @@ -55,8 +50,8 @@ function IndexerSettings() { }, []); const handleTestAllIndexersPress = useCallback(() => { - dispatch(testAllIndexers()); - }, [dispatch]); + testAllIndexers(); + }, [testAllIndexers]); return ( @@ -70,7 +65,7 @@ function IndexerSettings() { diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx index f75623539..c415bc3d1 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx @@ -1,13 +1,12 @@ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; import Button from 'Components/Link/Button'; import Link from 'Components/Link/Link'; import Menu from 'Components/Menu/Menu'; import MenuContent from 'Components/Menu/MenuContent'; import { sizes } from 'Helpers/Props'; -import { selectIndexerSchema } from 'Store/Actions/settingsActions'; -import Indexer from 'typings/Indexer'; +import { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { IndexerModel } from '../useIndexers'; import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; import styles from './AddIndexerItem.css'; @@ -15,8 +14,8 @@ interface AddIndexerItemProps { implementation: string; implementationName: string; infoLink: string; - presets?: Indexer[]; - onIndexerSelect: () => void; + presets?: IndexerModel[]; + onIndexerSelect: (selectedSchema: SelectedSchema) => void; } function AddIndexerItem({ @@ -26,19 +25,11 @@ function AddIndexerItem({ presets, onIndexerSelect, }: AddIndexerItemProps) { - const dispatch = useDispatch(); const hasPresets = !!presets && !!presets.length; const handleIndexerSelect = useCallback(() => { - dispatch( - selectIndexerSchema({ - implementation, - implementationName, - }) - ); - - onIndexerSelect(); - }, [implementation, implementationName, dispatch, onIndexerSelect]); + onIndexerSelect({ implementation, implementationName }); + }, [implementation, implementationName, onIndexerSelect]); return (
diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx index f834c30cb..f18857d4a 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx @@ -1,11 +1,11 @@ import React from 'react'; import Modal from 'Components/Modal/Modal'; -import AddIndexerModalContent from './AddIndexerModalContent'; +import AddIndexerModalContent, { + AddIndexerModalContentProps, +} from './AddIndexerModalContent'; -interface AddIndexerModalProps { +interface AddIndexerModalProps extends AddIndexerModalContentProps { isOpen: boolean; - onIndexerSelect: () => void; - onModalClose: () => void; } function AddIndexerModal({ diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx index 78c4d7ceb..686023b93 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx @@ -1,6 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import React, { useMemo } from 'react'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Button from 'Components/Link/Button'; @@ -10,14 +8,14 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; -import { fetchIndexerSchema } from 'Store/Actions/settingsActions'; -import Indexer from 'typings/Indexer'; +import { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { IndexerModel, useIndexerSchema } from '../useIndexers'; import AddIndexerItem from './AddIndexerItem'; import styles from './AddIndexerModalContent.css'; -interface AddIndexerModalContentProps { - onIndexerSelect: () => void; +export interface AddIndexerModalContentProps { + onIndexerSelect: (selectedSchema: SelectedSchema) => void; onModalClose: () => void; } @@ -25,15 +23,13 @@ function AddIndexerModalContent({ onIndexerSelect, onModalClose, }: AddIndexerModalContentProps) { - const dispatch = useDispatch(); - - const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = - useSelector((state: AppState) => state.settings.indexers); + const { isSchemaFetching, isSchemaFetched, schemaError, schema } = + useIndexerSchema(); const { usenetIndexers, torrentIndexers } = useMemo(() => { return schema.reduce<{ - usenetIndexers: Indexer[]; - torrentIndexers: Indexer[]; + usenetIndexers: IndexerModel[]; + torrentIndexers: IndexerModel[]; }>( (acc, item) => { if (item.protocol === 'usenet') { @@ -51,10 +47,6 @@ function AddIndexerModalContent({ ); }, [schema]); - useEffect(() => { - dispatch(fetchIndexerSchema()); - }, [dispatch]); - return ( {translate('AddIndexer')} @@ -66,7 +58,7 @@ function AddIndexerModalContent({ {translate('AddIndexerError')} ) : null} - {isSchemaPopulated && !schemaError ? ( + {isSchemaFetched && !schemaError ? (
{translate('SupportedIndexers')}
diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx index 70502a615..0c52ce69c 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx @@ -1,14 +1,12 @@ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem'; -import { selectIndexerSchema } from 'Store/Actions/settingsActions'; +import MenuItem from 'Components/Menu/MenuItem'; +import { SelectedSchema } from 'Settings/useProviderSchema'; -interface AddIndexerPresetMenuItemProps - extends Omit { +interface AddIndexerPresetMenuItemProps { name: string; implementation: string; implementationName: string; - onPress: () => void; + onPress: (selectedSchema: SelectedSchema) => void; } function AddIndexerPresetMenuItem({ @@ -18,19 +16,9 @@ function AddIndexerPresetMenuItem({ onPress, ...otherProps }: AddIndexerPresetMenuItemProps) { - const dispatch = useDispatch(); - const handlePress = useCallback(() => { - dispatch( - selectIndexerSchema({ - implementation, - implementationName, - presetName: name, - }) - ); - - onPress(); - }, [name, implementation, implementationName, dispatch, onPress]); + onPress({ implementation, implementationName, presetName: name }); + }, [name, implementation, implementationName, onPress]); return ( diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx index 87175e197..04357de18 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx @@ -1,18 +1,10 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import Modal from 'Components/Modal/Modal'; import { sizes } from 'Helpers/Props'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { - cancelSaveIndexer, - cancelTestIndexer, -} from 'Store/Actions/settingsActions'; import EditIndexerModalContent, { EditIndexerModalContentProps, } from './EditIndexerModalContent'; -const section = 'settings.indexers'; - interface EditIndexerModalProps extends EditIndexerModalContentProps { isOpen: boolean; } @@ -22,22 +14,9 @@ function EditIndexerModal({ onModalClose, ...otherProps }: EditIndexerModalProps) { - const dispatch = useDispatch(); - - const handleModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section })); - dispatch(cancelTestIndexer({ section })); - dispatch(cancelSaveIndexer({ section })); - - onModalClose(); - }, [dispatch, onModalClose]); - return ( - - + + ); } diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx index 460be11d5..854035556 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx @@ -1,7 +1,4 @@ import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { IndexerAppState } from 'App/State/SettingsAppState'; -import Alert from 'Components/Alert'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -9,7 +6,6 @@ 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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; @@ -18,44 +14,41 @@ import usePrevious from 'Helpers/Hooks/usePrevious'; import { inputTypes, kinds } from 'Helpers/Props'; import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; -import { - saveIndexer, - setIndexerFieldValue, - setIndexerValue, - testIndexer, -} from 'Store/Actions/settingsActions'; -import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; -import Indexer from 'typings/Indexer'; -import { InputChanged } from 'typings/inputs'; +import { SelectedSchema } from 'Settings/useProviderSchema'; +import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; +import { useManageIndexer } from '../useIndexers'; import styles from './EditIndexerModalContent.css'; export interface EditIndexerModalContentProps { id?: number; + cloneId?: number; + selectedSchema?: SelectedSchema; onModalClose: () => void; onDeleteIndexerPress?: () => void; } function EditIndexerModalContent({ id, + cloneId, + selectedSchema, onModalClose, onDeleteIndexerPress, }: EditIndexerModalContentProps) { - const dispatch = useDispatch(); const showAdvancedSettings = useShowAdvancedSettings(); const { - isFetching, - error, - isSaving, - isTesting = false, - saveError, item, + updateFieldValue, + updateValue, + saveProvider, + isSaving, + saveError, + testProvider, + isTesting, validationErrors, validationWarnings, - } = useSelector( - createProviderSettingsSelectorHook('indexers', id) - ); + } = useManageIndexer(id, cloneId, selectedSchema); const wasSaving = usePrevious(isSaving); @@ -77,27 +70,30 @@ function EditIndexerModalContent({ const handleInputChange = useCallback( (change: InputChanged) => { - // @ts-expect-error - actions are not typed - dispatch(setIndexerValue(change)); + // @ts-expect-error - InputChanged is not typed correctly + updateValue(change.name, change.value); }, - [dispatch] + [updateValue] ); const handleFieldChange = useCallback( - (change: InputChanged) => { - // @ts-expect-error - actions are not typed - dispatch(setIndexerFieldValue(change)); + ({ + name, + value, + additionalProperties, + }: EnhancedSelectInputChanged) => { + updateFieldValue({ [name]: value, ...additionalProperties }); }, - [dispatch] + [updateFieldValue] ); const handleSavePress = useCallback(() => { - dispatch(saveIndexer({ id })); - }, [id, dispatch]); + saveProvider(); + }, [saveProvider]); const handleTestPress = useCallback(() => { - dispatch(testIndexer({ id })); - }, [id, dispatch]); + testProvider(); + }, [testProvider]); useEffect(() => { if (!isSaving && wasSaving && !saveError) { @@ -114,169 +110,152 @@ function EditIndexerModalContent({ - {isFetching ? : null} +
+ + {translate('Name')} - {!isFetching && error ? ( - {translate('AddIndexerError')} - ) : null} + + - {!isFetching && !error ? ( - - - {translate('Name')} + + {translate('EnableRss')} - + + + + {translate('EnableAutomaticSearch')} + + + + + + {translate('EnableInteractiveSearch')} + + + + + {fields?.map((field) => { + return ( + - + ); + })} - - {translate('EnableRss')} + + {translate('IndexerPriority')} - - + + - - {translate('EnableAutomaticSearch')} + + {translate('MaximumSingleEpisodeAge')} - - + + - - {translate('EnableInteractiveSearch')} + + {translate('DownloadClient')} - - + + - {fields?.map((field) => { - return ( - - ); - })} + + {translate('Tags')} - - {translate('IndexerPriority')} - - - - - - {translate('MaximumSingleEpisodeAge')} - - - - - - {translate('DownloadClient')} - - - - - - {translate('Tags')} - - - - - ) : null} + + +
diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.tsx b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx index a9f068888..f3da6fa9b 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx @@ -1,15 +1,14 @@ import React, { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import Card from 'Components/Card'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; import { icons, kinds } from 'Helpers/Props'; -import { deleteIndexer } from 'Store/Actions/settingsActions'; import { useTagList } from 'Tags/useTags'; -import IndexerModel from 'typings/Indexer'; import translate from 'Utilities/String/translate'; +import { IndexerModel, useDeleteIndexer } from '../useIndexers'; import EditIndexerModal from './EditIndexerModal'; import styles from './Indexer.css'; @@ -21,6 +20,7 @@ interface IndexerProps extends IndexerModel { function Indexer({ id, name, + protocol, enableRss, enableAutomaticSearch, enableInteractiveSearch, @@ -31,8 +31,8 @@ function Indexer({ showPriority, onCloneIndexerPress, }: IndexerProps) { - const dispatch = useDispatch(); const tagList = useTagList(); + const { deleteIndexer } = useDeleteIndexer(id); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = @@ -56,8 +56,8 @@ function Indexer({ }, []); const handleConfirmDeleteIndexer = useCallback(() => { - dispatch(deleteIndexer({ id })); - }, [id, dispatch]); + deleteIndexer(); + }, [deleteIndexer]); const handleCloneIndexerPress = useCallback(() => { onCloneIndexerPress(id); @@ -75,12 +75,15 @@ function Indexer({
+ + {supportsRss && enableRss ? ( ) : null} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.tsx b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx index 9483e2ff8..8c9cb4286 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexers.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx @@ -1,49 +1,42 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { IndexerAppState } from 'App/State/SettingsAppState'; +import React, { useCallback, useState } 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 { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import IndexerModel from 'typings/Indexer'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { useSortedIndexers } from '../useIndexers'; import AddIndexerModal from './AddIndexerModal'; import EditIndexerModal from './EditIndexerModal'; import Indexer from './Indexer'; import styles from './Indexers.css'; function Indexers() { - const dispatch = useDispatch(); - - const { isFetching, isPopulated, items, error } = useSelector( - createSortedSectionSelector( - 'settings.indexers', - sortByProp('name') - ) - ); + const { isFetching, isFetched, data, error } = useSortedIndexers(); const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [cloneIndexerId, setCloneIndexerId] = useState(null); - const showPriority = items.some((index) => index.priority !== 25); + const showPriority = data.some((index) => index.priority !== 25); + + const [selectedSchema, setSelectedSchema] = useState< + SelectedSchema | undefined + >(undefined); const handleAddIndexerPress = useCallback(() => { + setCloneIndexerId(null); setIsAddIndexerModalOpen(true); }, []); - const handleCloneIndexerPress = useCallback( - (id: number) => { - dispatch(cloneIndexer({ id })); - setIsEditIndexerModalOpen(true); - }, - [dispatch] - ); + const handleCloneIndexerPress = useCallback((id: number) => { + setCloneIndexerId(id); + setIsEditIndexerModalOpen(true); + }, []); - const handleIndexerSelect = useCallback(() => { + const handleIndexerSelect = useCallback((selected: SelectedSchema) => { + setSelectedSchema(selected); setIsAddIndexerModalOpen(false); setIsEditIndexerModalOpen(true); }, []); @@ -53,23 +46,20 @@ function Indexers() { }, []); const handleEditIndexerModalClose = useCallback(() => { + setCloneIndexerId(null); setIsEditIndexerModalOpen(false); }, []); - useEffect(() => { - dispatch(fetchIndexers()); - }, [dispatch]); - return (
- {items.map((item) => { + {data.map((item) => { return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 24ea41a28..60a2739fd 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { SelectProvider, useSelect } from 'App/Select/SelectContext'; -import { IndexerAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; @@ -16,12 +14,16 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { kinds } from 'Helpers/Props'; import { - bulkDeleteIndexers, - bulkEditIndexers, + IndexerModel, + useBulkDeleteIndexers, + useBulkEditIndexers, + useIndexersData, + useSortedIndexers, +} from 'Settings/Indexers/useIndexers'; +import { setManageIndexersSort, -} from 'Store/Actions/settingsActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import Indexer from 'typings/Indexer'; + useManageIndexersOptions, +} from 'Settings/Indexers/useManageIndexersOptionsStore'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; @@ -37,6 +39,12 @@ const COLUMNS: Column[] = [ isSortable: true, isVisible: true, }, + { + name: 'protocol', + label: () => translate('Protocol'), + isSortable: true, + isVisible: true, + }, { name: 'implementation', label: () => translate('Implementation'), @@ -94,19 +102,11 @@ function ManageIndexersModalContentInner( ) { const { onModalClose } = props; - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: IndexerAppState = useSelector( - createClientSideCollectionSelector('settings.indexers') - ); - const dispatch = useDispatch(); + const { sortKey, sortDirection } = useManageIndexersOptions(); + const { data, isFetching, isFetched, error } = useSortedIndexers(); + + const { isDeleting, bulkDeleteIndexers } = useBulkDeleteIndexers(); + const { isSaving, bulkEditIndexers } = useBulkEditIndexers(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); @@ -121,14 +121,11 @@ function ManageIndexersModalContentInner( selectAll, unselectAll, useSelectedIds, - } = useSelect(); + } = useSelect(); - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageIndexersSort({ sortKey: value })); - }, - [dispatch] - ); + const onSortPress = useCallback((value: string) => { + setManageIndexersSort({ sortKey: value }); + }, []); const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); @@ -147,22 +144,20 @@ function ManageIndexersModalContentInner( }, [setIsEditModalOpen]); const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteIndexers({ ids: getSelectedIds() })); + bulkDeleteIndexers({ ids: getSelectedIds() }); setIsDeleteModalOpen(false); - }, [getSelectedIds, dispatch]); + }, [bulkDeleteIndexers, getSelectedIds]); const onSavePress = useCallback( (payload: object) => { setIsEditModalOpen(false); - dispatch( - bulkEditIndexers({ - ids: getSelectedIds(), - ...payload, - }) - ); + bulkEditIndexers({ + ids: getSelectedIds(), + ...payload, + }); }, - [getSelectedIds, dispatch] + [getSelectedIds, bulkEditIndexers] ); const onTagsPress = useCallback(() => { @@ -178,15 +173,13 @@ function ManageIndexersModalContentInner( setIsSavingTags(true); setIsTagsModalOpen(false); - dispatch( - bulkEditIndexers({ - ids: getSelectedIds(), - tags, - applyTags, - }) - ); + bulkEditIndexers({ + ids: getSelectedIds(), + tags, + applyTags, + }); }, - [getSelectedIds, dispatch] + [getSelectedIds, bulkEditIndexers] ); const onSelectAllChange = useCallback( @@ -211,11 +204,11 @@ function ManageIndexersModalContentInner( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isFetched && !error && !data.length ? ( {translate('NoIndexersFound')} ) : null} - {isPopulated && !!items.length && !isFetching && !isFetching ? ( + {isFetched && !!data.length && !isFetching && !isFetching ? (
- {items.map((item) => { + {data.map((item) => { return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css index cf3792c16..ec8097224 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css @@ -1,4 +1,5 @@ .name, +.protocol, .tags, .enableRss, .enableAutomaticSearch, diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css.d.ts b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css.d.ts index c1083bacf..75fc726cd 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css.d.ts +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.css.d.ts @@ -7,6 +7,7 @@ interface CssExports { 'implementation': string; 'name': string; 'priority': string; + 'protocol': string; 'seasonSearchMaximumSingleEpisodeAge': string; 'tags': string; } diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx index 730b510cd..c3e5a544a 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import { useSelect } from 'App/Select/SelectContext'; import Label from 'Components/Label'; import SeriesTagList from 'Components/SeriesTagList'; @@ -6,8 +7,9 @@ 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 DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { kinds } from 'Helpers/Props'; -import Indexer from 'typings/Indexer'; +import { IndexerModel } from 'Settings/Indexers/useIndexers'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import styles from './ManageIndexersModalRow.css'; @@ -15,6 +17,7 @@ import styles from './ManageIndexersModalRow.css'; interface ManageIndexersModalRowProps { id: number; name: string; + protocol: DownloadProtocol; enableRss: boolean; enableAutomaticSearch: boolean; enableInteractiveSearch: boolean; @@ -29,6 +32,7 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { const { id, name, + protocol, enableRss, enableAutomaticSearch, enableInteractiveSearch, @@ -38,7 +42,7 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { tags, } = props; - const { toggleSelected, useIsSelected } = useSelect(); + const { toggleSelected, useIsSelected } = useSelect(); const isSelected = useIsSelected(id); const onSelectedChangeWrapper = useCallback( @@ -62,6 +66,10 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { {name} + + + + {implementation} diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx index a3657b524..96a7097ff 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx @@ -1,8 +1,5 @@ import { uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { IndexerAppState } from 'App/State/SettingsAppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -15,8 +12,8 @@ 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 { useIndexersData } from 'Settings/Indexers/useIndexers'; import { Tag, useTagList } from 'Tags/useTags'; -import Indexer from 'typings/Indexer'; import translate from 'Utilities/String/translate'; import styles from './TagsModalContent.css'; @@ -26,12 +23,31 @@ interface TagsModalContentProps { onModalClose: () => void; } +const applyTagsOptions: EnhancedSelectInputValue[] = [ + { + key: 'add', + get value() { + return translate('Add'); + }, + }, + { + key: 'remove', + get value() { + return translate('Remove'); + }, + }, + { + key: 'replace', + get value() { + return translate('Replace'); + }, + }, +]; + function TagsModalContent(props: TagsModalContentProps) { const { ids, onModalClose, onApplyTagsPress } = props; - const allIndexers: IndexerAppState = useSelector( - (state: AppState) => state.settings.indexers - ); + const allIndexers = useIndexersData(); const tagList: Tag[] = useTagList(); const [tags, setTags] = useState([]); @@ -39,7 +55,7 @@ function TagsModalContent(props: TagsModalContentProps) { const indexersTags = useMemo(() => { const tags = ids.reduce((acc: number[], id) => { - const s = allIndexers.items.find((s: Indexer) => s.id === id); + const s = allIndexers.find((s) => s.id === id); if (s) { acc.push(...s.tags); @@ -69,27 +85,6 @@ function TagsModalContent(props: TagsModalContentProps) { onApplyTagsPress(tags, applyTags); }, [tags, applyTags, onApplyTagsPress]); - const applyTagsOptions: EnhancedSelectInputValue[] = [ - { - key: 'add', - get value() { - return translate('Add'); - }, - }, - { - key: 'remove', - get value() { - return translate('Remove'); - }, - }, - { - key: 'replace', - get value() { - return translate('Replace'); - }, - }, - ]; - return ( {translate('Tags')} diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx index 88cc7d63c..1719f6148 100644 --- a/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx +++ b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -9,21 +8,13 @@ import FormLabel from 'Components/Form/FormLabel'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { inputTypes, kinds } from 'Helpers/Props'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { - fetchIndexerOptions, - saveIndexerOptions, - setIndexerOptionsValue, -} from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { InputChanged } from 'typings/inputs'; import { OnChildStateChange, SetChildSave, } from 'typings/Settings/SettingsState'; import translate from 'Utilities/String/translate'; - -const SECTION = 'indexerOptions'; +import { useManageIndexerSettings } from './useIndexerSettings'; interface IndexerOptionsProps { setChildSave: SetChildSave; @@ -34,31 +25,31 @@ function IndexerOptions({ setChildSave, onChildStateChange, }: IndexerOptionsProps) { - const dispatch = useDispatch(); const { isFetching, - isPopulated, + isFetched, isSaving, error, settings, hasSettings, hasPendingChanges, - } = useSelector(createSettingsSectionSelector(SECTION)); + saveSettings, + updateSetting, + } = useManageIndexerSettings(); const showAdvancedSettings = useShowAdvancedSettings(); const handleInputChange = useCallback( - (change: InputChanged) => { - // @ts-expect-error - actions aren't typed - dispatch(setIndexerOptionsValue(change)); + ({ name, value }: InputChanged) => { + // @ts-expect-error - InputChanged name/value are not typed as keyof IndexerSettingsModel + updateSetting(name, value); }, - [dispatch] + [updateSetting] ); useEffect(() => { - dispatch(fetchIndexerOptions()); - setChildSave(() => dispatch(saveIndexerOptions())); - }, [dispatch, setChildSave]); + setChildSave(saveSettings); + }, [saveSettings, setChildSave]); useEffect(() => { onChildStateChange({ @@ -67,12 +58,6 @@ function IndexerOptions({ }); }, [hasPendingChanges, isSaving, onChildStateChange]); - useEffect(() => { - return () => { - dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); - }; - }, [dispatch]); - return (
{isFetching ? : null} @@ -83,7 +68,7 @@ function IndexerOptions({ ) : null} - {hasSettings && isPopulated && !error ? ( + {hasSettings && isFetched && !error ? (
{translate('MinimumAge')} diff --git a/frontend/src/Settings/Indexers/Options/useIndexerSettings.ts b/frontend/src/Settings/Indexers/Options/useIndexerSettings.ts new file mode 100644 index 000000000..2bcd225a1 --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/useIndexerSettings.ts @@ -0,0 +1,18 @@ +import { useManageSettings, useSettings } from 'Settings/useSettings'; + +export interface IndexerSettingsModel { + minimumAge: number; + retention: number; + maximumSize: number; + rssSyncInterval: number; +} + +const PATH = '/settings/indexer'; + +export const useIndexerSettings = () => { + return useSettings(PATH); +}; + +export const useManageIndexerSettings = () => { + return useManageSettings(PATH); +}; diff --git a/frontend/src/Settings/Indexers/useIndexerFlags.ts b/frontend/src/Settings/Indexers/useIndexerFlags.ts new file mode 100644 index 000000000..454a9614e --- /dev/null +++ b/frontend/src/Settings/Indexers/useIndexerFlags.ts @@ -0,0 +1,25 @@ +import useApiQuery from 'Helpers/Hooks/useApiQuery'; + +export interface IndexerFlag { + id: number; + name: string; +} + +const DEFAULT_INDEXER_FLAGS: IndexerFlag[] = []; + +const useIndexerFlags = () => { + const result = useApiQuery({ + path: '/indexerFlag', + queryOptions: { + gcTime: Infinity, + staleTime: Infinity, + }, + }); + + return { + ...result, + data: result.data ?? DEFAULT_INDEXER_FLAGS, + }; +}; + +export default useIndexerFlags; diff --git a/frontend/src/Settings/Indexers/useIndexers.ts b/frontend/src/Settings/Indexers/useIndexers.ts new file mode 100644 index 000000000..b0bf26064 --- /dev/null +++ b/frontend/src/Settings/Indexers/useIndexers.ts @@ -0,0 +1,274 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import { + SelectedSchema, + useProviderSchema, + useSelectedSchema, +} from 'Settings/useProviderSchema'; +import { + useDeleteProvider, + useManageProviderSettings, + useProviderSettings, +} from 'Settings/useProviderSettings'; +import Provider from 'typings/Provider'; +import { sortByProp } from 'Utilities/Array/sortByProp'; +import { ApiError } from 'Utilities/Fetch/fetchJson'; +import translate from 'Utilities/String/translate'; + +export interface IndexerModel extends Provider { + enableRss: boolean; + enableAutomaticSearch: boolean; + enableInteractiveSearch: boolean; + supportsRss: boolean; + supportsSearch: boolean; + seasonSearchMaximumSingleEpisodeAge: number; + protocol: DownloadProtocol; + priority: number; + downloadClientId: number; + tags: number[]; +} + +interface BulkEditIndexersPayload { + ids: number[]; + [key: string]: unknown; +} + +interface BulkDeleteIndexersPayload { + ids: number[]; +} + +const PATH = '/indexer'; + +export const useIndexersWithIds = (ids: number[]) => { + const allIndexers = useIndexersData(); + + return allIndexers.filter((indexer) => ids.includes(indexer.id)); +}; + +export const useIndexer = (id: number | undefined) => { + const { data } = useIndexers(); + + if (id === undefined) { + return undefined; + } + + return data.find((indexer) => indexer.id === id); +}; + +export const useIndexersData = () => { + const { data } = useIndexers(); + + return data; +}; + +export const useSortedIndexers = () => { + const result = useIndexers(); + + const sortedData = useMemo( + () => result.data.sort(sortByProp('name')), + [result.data] + ); + + return { + ...result, + data: sortedData, + }; +}; + +export const useIndexers = () => { + return useProviderSettings({ + path: PATH, + }); +}; + +export const useManageIndexer = ( + id: number | undefined, + cloneId: number | undefined, + selectedSchema?: SelectedSchema +) => { + const schema = useSelectedSchema(PATH, selectedSchema); + const cloneIndexer = useIndexer(cloneId); + + if (cloneId && !cloneIndexer) { + throw new Error(`Indexer with ID ${cloneId} not found`); + } + + if (selectedSchema && !schema) { + throw new Error('A selected schema is required to manage metadata'); + } + + const defaultProvider = useMemo(() => { + if (cloneId && cloneIndexer) { + const clonedIndexer = { + ...cloneIndexer, + id: 0, + name: translate('DefaultNameCopiedProfile', { + name: cloneIndexer.name, + }), + }; + + clonedIndexer.fields = clonedIndexer.fields.map((field) => { + const newField = { ...field }; + + if (newField.privacy === 'apiKey' || newField.privacy === 'password') { + newField.value = ''; + } + + return newField; + }); + + return clonedIndexer; + } + + if (selectedSchema && schema) { + return { + ...schema, + name: schema.implementationName, + enableRss: schema.supportsRss, + enableAutomaticSearch: schema.supportsSearch, + enableInteractiveSearch: schema.supportsSearch, + }; + } + + return {} as IndexerModel; + }, [cloneId, cloneIndexer, schema, selectedSchema]); + + const manage = useManageProviderSettings( + id, + defaultProvider, + PATH + ); + + return manage; +}; + +export const useDeleteIndexer = (id: number) => { + const result = useDeleteProvider(id, PATH); + + return { + ...result, + deleteIndexer: result.deleteProvider, + }; +}; + +export const useIndexerSchema = (enabled: boolean = true) => { + return useProviderSchema(PATH, enabled); +}; + +export const useTestIndexer = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const { mutate, isPending, error } = useApiMutation({ + path: `${PATH}/test`, + method: 'POST', + mutationOptions: { + onSuccess, + onError, + }, + }); + + return { + testIndexer: mutate, + isTesting: isPending, + testError: error, + }; +}; + +export const useTestAllIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const { mutate, isPending, error } = useApiMutation({ + path: `${PATH}/testall`, + method: 'POST', + mutationOptions: { + onSuccess, + onError, + }, + }); + + return { + testAllIndexers: mutate, + isTestingAllIndexers: isPending, + testAllError: error, + }; +}; + +export const useBulkEditIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const queryClient = useQueryClient(); + + const { mutate, isPending, error } = useApiMutation< + IndexerModel[], + BulkEditIndexersPayload + >({ + path: `${PATH}/bulk`, + method: 'PUT', + mutationOptions: { + onSuccess: (updatedIndexers) => { + queryClient.setQueryData([PATH], (oldIndexers) => { + if (!oldIndexers) { + return oldIndexers; + } + + return oldIndexers.map((indexer) => { + const updatedIndexer = updatedIndexers.find( + (updated) => updated.id === indexer.id + ); + + return updatedIndexer ? { ...indexer, ...updatedIndexer } : indexer; + }); + }); + onSuccess?.(); + }, + onError, + }, + }); + + return { + bulkEditIndexers: mutate, + isSaving: isPending, + bulkError: error, + }; +}; + +export const useBulkDeleteIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const queryClient = useQueryClient(); + + const { mutate, isPending, error } = useApiMutation< + void, + BulkDeleteIndexersPayload + >({ + path: `${PATH}/bulk`, + method: 'DELETE', + mutationOptions: { + onSuccess: (_, variables) => { + const deletedIds = new Set(variables.ids); + + queryClient.setQueryData([PATH], (oldIndexers) => { + if (!oldIndexers) { + return oldIndexers; + } + + return oldIndexers.filter((indexer) => !deletedIds.has(indexer.id)); + }); + onSuccess?.(); + }, + onError, + }, + }); + + return { + bulkDeleteIndexers: mutate, + isDeleting: isPending, + bulkDeleteError: error, + }; +}; diff --git a/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts b/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts new file mode 100644 index 000000000..6f95426d1 --- /dev/null +++ b/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts @@ -0,0 +1,20 @@ +import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore'; +import { SortDirection } from 'Helpers/Props/sortDirections'; + +export interface ManageIndexersOptions { + sortKey: string; + sortDirection: SortDirection; +} + +const { useOptions, setSort } = createOptionsStore( + 'manage_indexers_options', + () => { + return { + sortKey: 'name', + sortDirection: 'ascending', + }; + } +); + +export const useManageIndexersOptions = useOptions; +export const setManageIndexersSort = setSort; diff --git a/frontend/src/Settings/MetadataSource/TheTvdb.tsx b/frontend/src/Settings/MetadataSource/TheTvdb.tsx index 7e686bde1..49fc2a8b1 100644 --- a/frontend/src/Settings/MetadataSource/TheTvdb.tsx +++ b/frontend/src/Settings/MetadataSource/TheTvdb.tsx @@ -1,14 +1,17 @@ import React from 'react'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import useTheme from 'Helpers/Hooks/useTheme'; import translate from 'Utilities/String/translate'; import styles from './TheTvdb.css'; function TheTvdb() { + const theme = useTheme(); + return (
diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx index e126a285e..ede25bbaf 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx @@ -15,7 +15,7 @@ interface AddNotificationItemProps { implementationName: string; infoLink: string; presets?: NotificationModel[]; - onNotificationSelect: (selectedScehema: SelectedSchema) => void; + onNotificationSelect: (selectedSchema: SelectedSchema) => void; } function AddNotificationItem({ diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx index 55e4dc6e8..7dca3e2c1 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx @@ -14,7 +14,7 @@ import AddNotificationItem from './AddNotificationItem'; import styles from './AddNotificationModalContent.css'; export interface AddNotificationModalContentProps { - onNotificationSelect: (selectedScehema: SelectedSchema) => void; + onNotificationSelect: (selectedSchema: SelectedSchema) => void; onModalClose: () => void; } diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx index a94b24247..2c5dc00d8 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx @@ -6,7 +6,7 @@ interface AddNotificationPresetMenuItemProps { name: string; implementation: string; implementationName: string; - onPress: (selectedScehema: SelectedSchema) => void; + onPress: (selectedSchema: SelectedSchema) => void; } function AddNotificationPresetMenuItem({ diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx index 00752acad..4cf2d711f 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx @@ -1,14 +1,10 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import Modal from 'Components/Modal/Modal'; import { sizes } from 'Helpers/Props'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditNotificationModalContent, { EditNotificationModalContentProps, } from './EditNotificationModalContent'; -const section = 'settings.notifications'; - interface EditNotificationModalProps extends EditNotificationModalContentProps { isOpen: boolean; } @@ -18,18 +14,11 @@ function EditNotificationModal({ onModalClose, ...otherProps }: EditNotificationModalProps) { - const dispatch = useDispatch(); - - const handleModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section })); - onModalClose(); - }, [dispatch, onModalClose]); - return ( - + ); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx index 73bf7b756..c2c5eb85c 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx @@ -37,9 +37,9 @@ function EditNotificationModalContent({ }: EditNotificationModalContentProps) { const showAdvancedSettings = useShowAdvancedSettings(); - const result = useManageConnection(id, selectedSchema); const { item, + updateFieldValue, updateValue, saveProvider, isSaving, @@ -48,12 +48,7 @@ function EditNotificationModalContent({ isTesting, validationErrors, validationWarnings, - } = result; - - // updateFieldValue is guaranteed to exist for NotificationModel since it extends Provider - const { updateFieldValue } = result as typeof result & { - updateFieldValue: (fieldProperties: Record) => void; - }; + } = useManageConnection(id, selectedSchema); const wasSaving = usePrevious(isSaving); diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css index 586f99e70..ab0fff7b4 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css @@ -12,6 +12,10 @@ margin-right: auto; } +.deleteButtonInfoIcon { + margin-left: 8px; +} + .formatItemSmall { display: none; } diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css.d.ts b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css.d.ts index 689c2e723..aa9d78a22 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css.d.ts +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'deleteButtonContainer': string; + 'deleteButtonInfoIcon': string; 'formGroupWrapper': string; 'formGroupsContainer': string; 'formatItemLarge': string; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.tsx b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.tsx index 7b657101a..582055a65 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.tsx @@ -4,6 +4,7 @@ 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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -11,9 +12,10 @@ 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 useMeasure from 'Helpers/Hooks/useMeasure'; import usePrevious from 'Helpers/Hooks/usePrevious'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; import useQualityProfileInUse from 'Settings/Profiles/Quality/useQualityProfileInUse'; import dimensions from 'Styles/Variables/dimensions'; import { InputChanged } from 'typings/inputs'; @@ -69,7 +71,8 @@ function EditQualityProfileModalContent({ saveProvider, } = useManageQualityProfile(id, cloneId); - const isInUse = useQualityProfileInUse(id); + const { seriesCount, importListCount } = useQualityProfileInUse(id); + const isInUse = seriesCount !== 0 || importListCount !== 0; const [measureHeaderRef, { height: headerHeight }] = useMeasure(); const [measureBodyRef, { height: bodyHeight }] = useMeasure(); @@ -699,6 +702,36 @@ function EditQualityProfileModalContent({ > {translate('Delete')} + + {isInUse ? ( + + {seriesCount ? ( +
+ {translate('QualityProfileUsedInCountSeries', { + count: seriesCount, + })} +
+ ) : null} + {importListCount ? ( +
+ {translate('QualityProfileUsedInCountImportLists', { + count: importListCount, + })} +
+ ) : null} +
+ } + anchor={ + + } + /> + ) : null}
) : null} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.tsx b/frontend/src/Settings/Profiles/Quality/QualityProfile.tsx index 36e0d6b02..14a70d16c 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfile.tsx +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.tsx @@ -76,6 +76,7 @@ function QualityProfile({ diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx index 09a363ded..da24aecf5 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.tsx @@ -75,6 +75,7 @@ function QualityProfileItem({ className={styles.createGroupButton} name={icons.GROUP} title={translate('Group')} + aria-label={translate('Group')} onPress={handleCreateGroupPress} /> )} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.tsx b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.tsx index 06a840a2c..801728802 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.tsx +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.tsx @@ -89,6 +89,7 @@ function QualityProfileItemGroup({ className={styles.deleteGroupButton} name={icons.UNGROUP} title={translate('Ungroup')} + aria-label={translate('Ungroup')} onPress={handleDeleteGroupPress} /> diff --git a/frontend/src/Settings/Profiles/Quality/useQualityProfileInUse.ts b/frontend/src/Settings/Profiles/Quality/useQualityProfileInUse.ts index 29322c190..d0154e46b 100644 --- a/frontend/src/Settings/Profiles/Quality/useQualityProfileInUse.ts +++ b/frontend/src/Settings/Profiles/Quality/useQualityProfileInUse.ts @@ -11,13 +11,18 @@ function useQualityProfileInUse(id: number | undefined) { return useMemo(() => { if (!id) { - return false; + return { + seriesCount: 0, + importsCount: 0, + }; } - return ( - series.some((s) => s.qualityProfileId === id) || - importLists.some((list) => list.qualityProfileId === id) - ); + return { + seriesCount: series.filter((s) => s.qualityProfileId === id).length, + importListCount: importLists.filter( + (list) => list.qualityProfileId === id + ).length, + }; }, [id, series, importLists]); } diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index e1e35ef58..0c8fec237 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -39,8 +39,17 @@ function EditReleaseProfileModalContent({ saveProvider, } = useManageReleaseProfile(id ?? 0); - const { name, enabled, required, ignored, indexerIds, tags, excludedTags } = - item; + const { + name, + enabled, + required, + ignored, + airDateRestriction, + airDateGracePeriod, + indexerIds, + tags, + excludedTags, + } = item; const wasSaving = usePrevious(isSaving); @@ -131,6 +140,33 @@ function EditReleaseProfileModalContent({ />
+ + {translate('AirDateRestriction')} + + + + + {airDateRestriction.value ? ( + + {translate('AirDateGracePeriod')} + + + + ) : null} + {translate('Indexer')} diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx index ae718b272..efb14c546 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx @@ -6,8 +6,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { kinds } from 'Helpers/Props'; +import { IndexerModel } from 'Settings/Indexers/useIndexers'; import { Tag } from 'Tags/useTags'; -import Indexer from 'typings/Indexer'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; import { @@ -18,7 +18,7 @@ import styles from './ReleaseProfileItem.css'; interface ReleaseProfileProps extends ReleaseProfileModel { tagList: Tag[]; - indexerList: Indexer[]; + indexerList: IndexerModel[]; } function ReleaseProfileItem(props: ReleaseProfileProps) { diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx index 877b09d3f..759394361 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -1,13 +1,11 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import React from 'react'; 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/settingsActions'; +import { useIndexersData } from 'Settings/Indexers/useIndexers'; import { useTagList } from 'Tags/useTags'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; @@ -16,13 +14,10 @@ import { useReleaseProfiles } from './useReleaseProfiles'; import styles from './ReleaseProfiles.css'; function ReleaseProfiles() { - const dispatch = useDispatch(); const { data, isFetching, isFetched, error } = useReleaseProfiles(); const tagList = useTagList(); - const indexerList = useSelector( - (state: AppState) => state.settings.indexers.items - ); + const indexerList = useIndexersData(); const [ isAddReleaseProfileModalOpen, @@ -30,10 +25,6 @@ function ReleaseProfiles() { setAddReleaseProfileModalClosed, ] = useModalOpenState(false); - useEffect(() => { - dispatch(fetchIndexers()); - }, [dispatch]); - return (
diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx index 099dfd5d8..b49bf27c6 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx @@ -70,6 +70,7 @@ export default function Specification({ diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx index 1bc9c2cb7..fd003ecb8 100644 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx @@ -12,6 +12,7 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { kinds } from 'Helpers/Props'; import useSeries from 'Series/useSeries'; +import { useIndexersWithIds } from 'Settings/Indexers/useIndexers'; import { useConnectionsWithIds } from 'Settings/Notifications/useConnections'; import { useReleaseProfilesWithIds } from 'Settings/Profiles/Release/useReleaseProfiles'; import translate from 'Utilities/String/translate'; @@ -99,13 +100,7 @@ function TagDetailsModalContent({ const releaseProfiles = useReleaseProfilesWithIds(releaseProfileIds); const notifications = useConnectionsWithIds(notificationIds); - - const indexers = useSelector( - createMatchingItemSelector( - indexerIds, - (state: AppState) => state.settings.indexers.items - ) - ); + const indexers = useIndexersWithIds(indexerIds); const downloadClients = useSelector( createMatchingItemSelector( diff --git a/frontend/src/Settings/Tags/Tags.tsx b/frontend/src/Settings/Tags/Tags.tsx index 73c89bcc0..5cdc51faa 100644 --- a/frontend/src/Settings/Tags/Tags.tsx +++ b/frontend/src/Settings/Tags/Tags.tsx @@ -5,13 +5,13 @@ import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { kinds } from 'Helpers/Props'; +import { useIndexers } from 'Settings/Indexers/useIndexers'; import { useConnections } from 'Settings/Notifications/useConnections'; import { useReleaseProfiles } from 'Settings/Profiles/Release/useReleaseProfiles'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, - fetchIndexers, } from 'Store/Actions/settingsActions'; import useTagDetails from 'Tags/useTagDetails'; import useTags, { useSortedTagList } from 'Tags/useTags'; @@ -33,11 +33,11 @@ function Tags() { useReleaseProfiles(); useConnections(); + useIndexers(); useEffect(() => { dispatch(fetchDelayProfiles()); dispatch(fetchImportLists()); - dispatch(fetchIndexers()); dispatch(fetchDownloadClients()); queryClient.invalidateQueries({ queryKey: ['releaseprofile'] }); diff --git a/frontend/src/Settings/UI/UISettings.tsx b/frontend/src/Settings/UI/UISettings.tsx index 74e16c1db..9c5711b8c 100644 --- a/frontend/src/Settings/UI/UISettings.tsx +++ b/frontend/src/Settings/UI/UISettings.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Form from 'Components/Form/Form'; @@ -11,8 +10,8 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import { inputTypes, kinds } from 'Helpers/Props'; +import { useFilteredLanguages } from 'Language/useLanguages'; import SettingsToolbar from 'Settings/SettingsToolbar'; -import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; import themes from 'Styles/Themes'; import { InputChanged } from 'typings/inputs'; import timeZoneOptions from 'Utilities/Date/timeZoneOptions'; @@ -63,17 +62,15 @@ export const timeFormatOptions: EnhancedSelectInputValue[] = [ function UISettings() { const { - items, + data: languageItems = [], isFetching: isLanguagesFetching, - isPopulated: isLanguagesPopulated, + isFetched: isLanguagesPopulated, error: languagesError, - } = useSelector( - createLanguagesSelector({ - Any: true, - Original: true, - Unknown: true, - }) - ); + } = useFilteredLanguages({ + Any: true, + Original: true, + Unknown: true, + }); const { isFetching: isSettingsFetching, @@ -94,13 +91,13 @@ function UISettings() { const error = languagesError || settingsError; const languages = useMemo(() => { - return items.map((item) => { + return languageItems.map((item) => { return { key: item.id, value: item.name, }; }); - }, [items]); + }, [languageItems]); const themeOptions = Object.keys(themes).map((theme) => ({ key: theme, @@ -261,6 +258,8 @@ function UISettings() { name="uiLanguage" helpText={translate('UiLanguageHelpText')} helpTextWarning={translate('BrowserReloadRequired')} + includeOriginal={false} + includeUnknown={false} onChange={handleInputChange} {...settings.uiLanguage} errors={ diff --git a/frontend/src/Settings/useProviderSettings.ts b/frontend/src/Settings/useProviderSettings.ts index 0756f9300..dced47a4b 100644 --- a/frontend/src/Settings/useProviderSettings.ts +++ b/frontend/src/Settings/useProviderSettings.ts @@ -1,16 +1,27 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo, useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo, useRef, useState } from 'react'; import ModelBase from 'App/ModelBase'; -import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import useApiMutation, { + addOrUpdateQueryClientItem, + getValidationFailures, +} from 'Helpers/Hooks/useApiMutation'; import useApiQuery, { QueryOptions } from 'Helpers/Hooks/useApiQuery'; import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore'; import { usePendingFieldsStore } from 'Helpers/Hooks/usePendingFieldsStore'; import selectSettings from 'Store/Selectors/selectSettings'; import { PendingSection } from 'typings/pending'; import Provider from 'typings/Provider'; -import { ApiError } from 'Utilities/Fetch/fetchJson'; +import fetchJson, { ApiError } from 'Utilities/Fetch/fetchJson'; +import getQueryPath from 'Utilities/Fetch/getQueryPath'; +import getQueryString, { QueryParams } from 'Utilities/Fetch/getQueryString'; -interface ManageProviderSettings +export type SkipValidation = 'none' | 'warnings' | 'all'; +export interface SaveOptions { + skipTesting?: boolean; + skipValidation?: SkipValidation; +} + +interface BaseManageProviderSettings extends Omit>, 'settings'> { item: PendingSection; updateValue: (key: K, value: T[K]) => void; @@ -19,9 +30,17 @@ interface ManageProviderSettings saveError: ApiError | null; testProvider: () => void; isTesting: boolean; - updateFieldValue?: (fieldProperties: Record) => void; } +interface ManageProviderSettingsWithFields + extends BaseManageProviderSettings { + updateFieldValue: (fieldProperties: Record) => void; +} + +type ManageProviderSettings = T extends Provider + ? ManageProviderSettingsWithFields + : BaseManageProviderSettings; + const isProviderWithFields = (provider: unknown): provider is Provider => { return ( typeof provider === 'object' && @@ -72,28 +91,54 @@ export const useSaveProviderSettings = ( ) => { const queryClient = useQueryClient(); - const { mutate, isPending, error } = useApiMutation({ - path: id ? `${path}/${id}` : path, - method: id ? 'PUT' : 'POST', - mutationOptions: { - onSuccess: (updatedSettings: T) => { - queryClient.setQueryData([path], (oldData = []) => { - if (id) { - return oldData.map((item) => - item.id === updatedSettings.id ? updatedSettings : item - ); - } + const { mutate, isPending, error } = useMutation< + T, + ApiError, + { + data: T; + } & SaveOptions + >({ + mutationFn: async ({ data, skipTesting, skipValidation }) => { + const queryParams: QueryParams = {}; - return [...oldData, updatedSettings]; - }); - onSuccess?.(updatedSettings); - }, - onError, + if (skipTesting) { + queryParams.skipTesting = true; + } + + if (skipValidation && skipValidation !== 'none') { + queryParams.skipValidation = skipValidation; + } + + return fetchJson({ + path: + getQueryPath(id ? `${path}/${id}` : path) + + getQueryString(queryParams), + method: id ? 'PUT' : 'POST', + headers: { + 'X-Api-Key': window.Sonarr.apiKey, + 'X-Sonarr-Client': 'Sonarr', + }, + body: data, + }); }, + onSuccess: (updatedSettings: T) => { + queryClient.setQueryData([path], (oldData = []) => + addOrUpdateQueryClientItem(oldData, updatedSettings, 'id') + ); + onSuccess?.(updatedSettings); + }, + onError, }); + const save = useCallback( + (data: T, options?: SaveOptions) => { + mutate({ data, ...options }); + }, + [mutate] + ); + return { - save: mutate, + save, isSaving: isPending, saveError: error, }; @@ -104,17 +149,41 @@ export const useTestProvider = ( onSuccess?: () => void, onError?: (error: ApiError) => void ) => { - const { mutate, isPending, error } = useApiMutation({ - path: `${path}/test`, - method: 'POST', - mutationOptions: { - onSuccess, - onError, + const { mutate, isPending, error } = useMutation< + void, + ApiError, + { data: T } & SaveOptions + >({ + mutationFn: async ({ data, skipValidation }) => { + const queryParams: QueryParams = {}; + + if (skipValidation && skipValidation !== 'none') { + queryParams.skipValidation = skipValidation; + } + + return fetchJson({ + path: getQueryPath(`${path}/test`) + getQueryString(queryParams), + method: 'POST', + headers: { + 'X-Api-Key': window.Sonarr.apiKey, + 'X-Sonarr-Client': 'Sonarr', + }, + body: data, + }); }, + onSuccess, + onError, }); + const test = useCallback( + (data: T, options?: SaveOptions) => { + mutate({ data, ...options }); + }, + [mutate] + ); + return { - test: mutate, + test, isTesting: isPending, testError: error, }; @@ -127,12 +196,14 @@ export const useManageProviderSettings = ( ): ManageProviderSettings => { const provider = useProviderWithDefault(id, defaultProvider, path); const [mutationError, setMutationError] = useState(null); + const lastSaveData = useRef(null); const { pendingChanges, setPendingChange, unsetPendingChange, clearPendingChanges, + hasPendingChanges, } = usePendingChangesStore({}); const { @@ -146,6 +217,7 @@ export const useManageProviderSettings = ( setMutationError(null); clearPendingChanges(); clearPendingFields(); + lastSaveData.current = null; }, [clearPendingChanges, clearPendingFields]); const handleTestSuccess = useCallback(() => { @@ -211,8 +283,40 @@ export const useManageProviderSettings = ( } as T; } - save(updatedSettings); - }, [provider, pendingChanges, pendingFields, save]); + const serializedSettings = JSON.stringify(updatedSettings); + const isResave = lastSaveData.current === serializedSettings; + lastSaveData.current = serializedSettings; + + const saveOptions: SaveOptions = {}; + + // For existing providers with no pending changes, skip testing and all validation. + if (provider.id > 0 && !hasPendingChanges && !hasPendingFields) { + saveOptions.skipTesting = true; + saveOptions.skipValidation = 'all'; + } else { + // If resaving the exact same settings as the previous attempt, skip testing. + if (isResave) { + saveOptions.skipTesting = true; + } + + // If the last save returned only warnings, skip warning validation on the next save. + const { errors, warnings } = getValidationFailures(mutationError); + + if (errors.length === 0 && warnings.length > 0) { + saveOptions.skipValidation = 'warnings'; + } + } + + save(updatedSettings, saveOptions); + }, [ + provider, + pendingChanges, + pendingFields, + hasPendingChanges, + hasPendingFields, + mutationError, + save, + ]); const testProvider = useCallback(() => { let updatedSettings: T = { @@ -238,8 +342,17 @@ export const useManageProviderSettings = ( } as T; } - test(updatedSettings); - }, [provider, pendingChanges, pendingFields, test]); + const testOptions: SaveOptions = {}; + + // If the last operation returned only warnings, skip warning validation on the next test. + const { errors, warnings } = getValidationFailures(mutationError); + + if (errors.length === 0 && warnings.length > 0) { + testOptions.skipValidation = 'warnings'; + } + + test(updatedSettings, testOptions); + }, [provider, pendingChanges, pendingFields, mutationError, test]); const updateValue = useCallback( (key: K, value: T[K]) => { @@ -296,10 +409,10 @@ export const useManageProviderSettings = ( return { ...baseReturn, updateFieldValue, - }; + } as ManageProviderSettings; } - return baseReturn; + return baseReturn as ManageProviderSettings; }; export const useDeleteProvider = ( diff --git a/frontend/src/Store/Actions/Settings/general.js b/frontend/src/Store/Actions/Settings/general.js deleted file mode 100644 index 98bb2703d..000000000 --- a/frontend/src/Store/Actions/Settings/general.js +++ /dev/null @@ -1,64 +0,0 @@ -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.general'; - -// -// Actions Types - -export const FETCH_GENERAL_SETTINGS = 'settings/general/fetchGeneralSettings'; -export const SET_GENERAL_SETTINGS_VALUE = 'settings/general/setGeneralSettingsValue'; -export const SAVE_GENERAL_SETTINGS = 'settings/general/saveGeneralSettings'; - -// -// Action Creators - -export const fetchGeneralSettings = createThunk(FETCH_GENERAL_SETTINGS); -export const saveGeneralSettings = createThunk(SAVE_GENERAL_SETTINGS); -export const setGeneralSettingsValue = createAction(SET_GENERAL_SETTINGS_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_GENERAL_SETTINGS]: createFetchHandler(section, '/config/host'), - [SAVE_GENERAL_SETTINGS]: createSaveHandler(section, '/config/host') - }, - - // - // Reducers - - reducers: { - [SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer(section) - } - -}; diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js deleted file mode 100644 index a89d65208..000000000 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ /dev/null @@ -1,110 +0,0 @@ -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'; -import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; - -// -// Variables - -const section = 'settings.importListExclusions'; - -// -// Actions Types - -export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions'; -export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage'; -export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort'; -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 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) => { - return { - section, - ...payload - }; -}); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - pageSize: 20, - items: [], - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - pendingChanges: {} - }, - - // - // Action Handlers - - actionHandlers: handleThunks({ - ...createServerSideCollectionHandlers( - section, - '/importlistexclusion/paged', - fetchImportListExclusions, - { - [serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS, - [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'), - [BULK_DELETE_IMPORT_LIST_EXCLUSIONS]: createBulkRemoveItemHandler(section, '/importlistexclusion/bulk') - }), - - // - // Reducers - - reducers: { - [SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(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/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js deleted file mode 100644 index a53fe1c61..000000000 --- a/frontend/src/Store/Actions/Settings/indexerFlags.js +++ /dev/null @@ -1,48 +0,0 @@ -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/Settings/indexerOptions.js b/frontend/src/Store/Actions/Settings/indexerOptions.js deleted file mode 100644 index bafc2735d..000000000 --- a/frontend/src/Store/Actions/Settings/indexerOptions.js +++ /dev/null @@ -1,64 +0,0 @@ -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.indexerOptions'; - -// -// Actions Types - -export const FETCH_INDEXER_OPTIONS = 'settings/indexerOptions/fetchIndexerOptions'; -export const SAVE_INDEXER_OPTIONS = 'settings/indexerOptions/saveIndexerOptions'; -export const SET_INDEXER_OPTIONS_VALUE = 'settings/indexerOptions/setIndexerOptionsValue'; - -// -// Action Creators - -export const fetchIndexerOptions = createThunk(FETCH_INDEXER_OPTIONS); -export const saveIndexerOptions = createThunk(SAVE_INDEXER_OPTIONS); -export const setIndexerOptionsValue = createAction(SET_INDEXER_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_INDEXER_OPTIONS]: createFetchHandler(section, '/config/indexer'), - [SAVE_INDEXER_OPTIONS]: createSaveHandler(section, '/config/indexer') - }, - - // - // Reducers - - reducers: { - [SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer(section) - } - -}; diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js deleted file mode 100644 index a277e013f..000000000 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ /dev/null @@ -1,180 +0,0 @@ -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 createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; -import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; -import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; -import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; -import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; -import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; -import getSectionState from 'Utilities/State/getSectionState'; -import selectProviderSchema from 'Utilities/State/selectProviderSchema'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import translate from 'Utilities/String/translate'; - -// -// Variables - -const section = 'settings.indexers'; - -// -// Actions Types - -export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers'; -export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema'; -export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema'; -export const CLONE_INDEXER = 'settings/indexers/cloneIndexer'; -export const SET_INDEXER_VALUE = 'settings/indexers/setIndexerValue'; -export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue'; -export const SAVE_INDEXER = 'settings/indexers/saveIndexer'; -export const CANCEL_SAVE_INDEXER = 'settings/indexers/cancelSaveIndexer'; -export const DELETE_INDEXER = 'settings/indexers/deleteIndexer'; -export const TEST_INDEXER = 'settings/indexers/testIndexer'; -export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; -export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; -export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers'; -export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers'; -export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort'; - -// -// Action Creators - -export const fetchIndexers = createThunk(FETCH_INDEXERS); -export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); -export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); -export const cloneIndexer = createAction(CLONE_INDEXER); - -export const saveIndexer = createThunk(SAVE_INDEXER); -export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER); -export const deleteIndexer = createThunk(DELETE_INDEXER); -export const testIndexer = createThunk(TEST_INDEXER); -export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); -export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); -export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS); -export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); -export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT); - -export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -export const setIndexerFieldValue = createAction(SET_INDEXER_FIELD_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - isSchemaFetching: false, - isSchemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - isTesting: false, - isTestingAll: false, - items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_INDEXERS]: createFetchHandler(section, '/indexer'), - [FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler(section, '/indexer/schema'), - - [SAVE_INDEXER]: createSaveProviderHandler(section, '/indexer'), - [CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler(section), - [DELETE_INDEXER]: createRemoveItemHandler(section, '/indexer'), - [TEST_INDEXER]: createTestProviderHandler(section, '/indexer'), - [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section), - [TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer'), - [BULK_EDIT_INDEXERS]: createBulkEditItemHandler(section, '/indexer/bulk'), - [BULK_DELETE_INDEXERS]: createBulkRemoveItemHandler(section, '/indexer/bulk') - }, - - // - // Reducers - - reducers: { - [SET_INDEXER_VALUE]: createSetSettingValueReducer(section), - [SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer(section), - - [SELECT_INDEXER_SCHEMA]: (state, { payload }) => { - return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = payload.presetName ?? payload.implementationName; - selectedSchema.implementationName = payload.implementationName; - selectedSchema.enableRss = selectedSchema.supportsRss; - selectedSchema.enableAutomaticSearch = selectedSchema.supportsSearch; - selectedSchema.enableInteractiveSearch = selectedSchema.supportsSearch; - - return selectedSchema; - }); - }, - - [CLONE_INDEXER]: function(state, { payload }) { - const id = payload.id; - const newState = getSectionState(state, section); - const item = newState.items.find((i) => i.id === id); - - // Use selectedSchema so `createProviderSettingsSelector` works properly - const selectedSchema = { ...item }; - delete selectedSchema.id; - delete selectedSchema.name; - - selectedSchema.fields = selectedSchema.fields.map((field) => { - const newField = { ...field }; - - if (newField.privacy === 'apiKey' || newField.privacy === 'password') { - newField.value = ''; - } - - return newField; - }); - - newState.selectedSchema = selectedSchema; - - // Set the name in pendingChanges - newState.pendingChanges = { - name: translate('DefaultNameCopiedProfile', { name: item.name }) - }; - - return updateSectionState(state, section, newState); - }, - - [SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section) - - } - -}; diff --git a/frontend/src/Store/Actions/Settings/languages.js b/frontend/src/Store/Actions/Settings/languages.js deleted file mode 100644 index a0b62fc49..000000000 --- a/frontend/src/Store/Actions/Settings/languages.js +++ /dev/null @@ -1,48 +0,0 @@ -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; -import { createThunk } from 'Store/thunks'; - -// -// Variables - -const section = 'settings.languages'; - -// -// Actions Types - -export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages'; - -// -// Action Creators - -export const fetchLanguages = createThunk(FETCH_LANGUAGES); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - items: [] - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_LANGUAGES]: createFetchHandler(section, '/language') - }, - - // - // Reducers - - reducers: { - - } - -}; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index d830f3124..294d4dca0 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -7,14 +7,8 @@ import customFormatSpecifications from './Settings/customFormatSpecifications'; import delayProfiles from './Settings/delayProfiles'; 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 indexerFlags from './Settings/indexerFlags'; -import indexerOptions from './Settings/indexerOptions'; -import indexers from './Settings/indexers'; -import languages from './Settings/languages'; export * from './Settings/autoTaggingSpecifications'; export * from './Settings/autoTaggings'; @@ -23,14 +17,8 @@ export * from './Settings/customFormats'; 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/indexerFlags'; -export * from './Settings/indexerOptions'; -export * from './Settings/indexers'; -export * from './Settings/languages'; // // Variables @@ -49,14 +37,8 @@ export const defaultState = { delayProfiles: delayProfiles.defaultState, downloadClients: downloadClients.defaultState, downloadClientOptions: downloadClientOptions.defaultState, - general: general.defaultState, importLists: importLists.defaultState, - importListExclusions: importListExclusions.defaultState, - importListOptions: importListOptions.defaultState, - indexerFlags: indexerFlags.defaultState, - indexerOptions: indexerOptions.defaultState, - indexers: indexers.defaultState, - languages: languages.defaultState + importListOptions: importListOptions.defaultState }; export const persistState = [ @@ -74,14 +56,8 @@ export const actionHandlers = handleThunks({ ...delayProfiles.actionHandlers, ...downloadClients.actionHandlers, ...downloadClientOptions.actionHandlers, - ...general.actionHandlers, ...importLists.actionHandlers, - ...importListExclusions.actionHandlers, - ...importListOptions.actionHandlers, - ...indexerFlags.actionHandlers, - ...indexerOptions.actionHandlers, - ...indexers.actionHandlers, - ...languages.actionHandlers + ...importListOptions.actionHandlers }); // @@ -95,13 +71,7 @@ export const reducers = createHandleActions({ ...delayProfiles.reducers, ...downloadClients.reducers, ...downloadClientOptions.reducers, - ...general.reducers, ...importLists.reducers, - ...importListExclusions.reducers, - ...importListOptions.reducers, - ...indexerFlags.reducers, - ...indexerOptions.reducers, - ...indexers.reducers, - ...languages.reducers + ...importListOptions.reducers }, defaultState, section); diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts deleted file mode 100644 index 90587639c..000000000 --- a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts +++ /dev/null @@ -1,9 +0,0 @@ -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/Store/Selectors/createLanguagesSelector.ts b/frontend/src/Store/Selectors/createLanguagesSelector.ts deleted file mode 100644 index 37a6c6135..000000000 --- a/frontend/src/Store/Selectors/createLanguagesSelector.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -interface LanguageFilter { - [key: string]: boolean | undefined; - Any: boolean; - Original?: boolean; - Unknown?: boolean; -} - -function createLanguagesSelector( - excludeLanguages: LanguageFilter = { Any: true } -) { - return createSelector( - (state: AppState) => state.settings.languages, - (languages) => { - const { isFetching, isPopulated, error, items } = languages; - - const filteredLanguages = items.filter( - (lang) => !excludeLanguages[lang.name] - ); - - return { - isFetching, - isPopulated, - error, - items: filteredLanguages, - }; - } - ); -} - -export default createLanguagesSelector; diff --git a/frontend/src/System/Backup/BackupRow.tsx b/frontend/src/System/Backup/BackupRow.tsx index 739d07ec9..4b2d682eb 100644 --- a/frontend/src/System/Backup/BackupRow.tsx +++ b/frontend/src/System/Backup/BackupRow.tsx @@ -96,12 +96,14 @@ function BackupRow({ id, type, name, path, size, time }: BackupRowProps) { diff --git a/frontend/src/System/Events/LogsTable.tsx b/frontend/src/System/Events/LogsTable.tsx index 128726857..3919bfcbb 100644 --- a/frontend/src/System/Events/LogsTable.tsx +++ b/frontend/src/System/Events/LogsTable.tsx @@ -38,6 +38,7 @@ function LogsTable() { isLoading, page, goToPage, + refetch, } = useEvents(); const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } = @@ -77,18 +78,15 @@ function LogsTable() { const handleRefreshPress = useCallback(() => { goToPage(1); - }, [goToPage]); + refetch(); + }, [goToPage, refetch]); const handleClearLogsPress = useCallback(() => { - executeCommand( - { - name: CommandNames.ClearLog, - }, - () => { - goToPage(1); - } - ); - }, [executeCommand, goToPage]); + executeCommand({ name: CommandNames.ClearLog }, () => { + goToPage(1); + refetch(); + }); + }, [executeCommand, goToPage, refetch]); return ( diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx index f6743e664..07ed81c24 100644 --- a/frontend/src/System/Status/Health/Health.tsx +++ b/frontend/src/System/Status/Health/Health.tsx @@ -14,10 +14,8 @@ 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 { useTestAllIndexers } from 'Settings/Indexers/useIndexers'; +import { testAllDownloadClients } from 'Store/Actions/settingsActions'; import titleCase from 'Utilities/String/titleCase'; import translate from 'Utilities/String/translate'; import HealthItemLink from './HealthItemLink'; @@ -49,9 +47,8 @@ function Health() { const isTestingAllDownloadClients = useSelector( (state: AppState) => state.settings.downloadClients.isTestingAll ); - const isTestingAllIndexers = useSelector( - (state: AppState) => state.settings.indexers.isTestingAll - ); + + const { testAllIndexers, isTestingAllIndexers } = useTestAllIndexers(); const healthIssues = !!data.length; @@ -60,8 +57,8 @@ function Health() { }, [dispatch]); const handleTestAllIndexersPress = useCallback(() => { - dispatch(testAllIndexers()); - }, [dispatch]); + testAllIndexers(); + }, [testAllIndexers]); return (
diff --git a/frontend/src/System/Status/Health/HealthItemLink.tsx b/frontend/src/System/Status/Health/HealthItemLink.tsx index ac3bafade..90a9b4c3a 100644 --- a/frontend/src/System/Status/Health/HealthItemLink.tsx +++ b/frontend/src/System/Status/Health/HealthItemLink.tsx @@ -20,6 +20,7 @@ function HealthItemLink(props: HealthItemLinkProps) { ); @@ -30,6 +31,7 @@ function HealthItemLink(props: HealthItemLinkProps) { ); @@ -38,6 +40,7 @@ function HealthItemLink(props: HealthItemLinkProps) { ); @@ -46,6 +49,7 @@ function HealthItemLink(props: HealthItemLinkProps) { ); @@ -54,6 +58,7 @@ function HealthItemLink(props: HealthItemLinkProps) { ); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx index 7b426edbf..0f15dde9f 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx @@ -219,6 +219,7 @@ export default function QueuedTaskRow(props: QueuedTaskRowProps) { {status === 'queued' && ( diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx index c4e5035ea..ce446fb14 100644 --- a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx +++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.tsx @@ -10,6 +10,7 @@ import { isCommandExecuting } from 'Utilities/Command'; import formatDate from 'Utilities/Date/formatDate'; import formatDateTime from 'Utilities/Date/formatDateTime'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import translate from 'Utilities/String/translate'; import styles from './ScheduledTaskRow.css'; interface ScheduledTaskRowProps { @@ -138,6 +139,7 @@ function ScheduledTaskRow({ diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index 7e99007a8..1bdbe5748 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useCallback, useMemo, useState } from 'react'; import { useAppValue } from 'App/appStore'; import CommandNames from 'Commands/CommandNames'; import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands'; @@ -13,11 +12,13 @@ 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 { + UpdateMechanism, + useGeneralSettings, +} from 'Settings/General/useGeneralSettings'; import useUpdateSettings from 'Settings/General/useUpdateSettings'; import { useUiSettingsValues } from 'Settings/UI/useUiSettings'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; import { useSystemStatusData } from 'System/Status/useSystemStatus'; -import { UpdateMechanism } from 'typings/Settings/General'; import formatDate from 'Utilities/Date/formatDate'; import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; @@ -49,7 +50,6 @@ function Updates() { error: settingsError, } = useUpdateSettings(); - const dispatch = useDispatch(); const executeCommand = useExecuteCommand(); const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); const isFetching = isLoadingUpdates || isLoadingSettings; @@ -107,9 +107,7 @@ function Updates() { setIsMajorUpdateModalOpen(false); }, [setIsMajorUpdateModalOpen]); - useEffect(() => { - dispatch(fetchGeneralSettings()); - }, [dispatch]); + useGeneralSettings(); return ( diff --git a/frontend/src/Tags/useTags.ts b/frontend/src/Tags/useTags.ts index 8e4f3da44..dddb9aa4e 100644 --- a/frontend/src/Tags/useTags.ts +++ b/frontend/src/Tags/useTags.ts @@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import ModelBase from 'App/ModelBase'; import useApiMutation, { + addOrUpdateQueryClientItem, getValidationFailures, } from 'Helpers/Hooks/useApiMutation'; import useApiQuery from 'Helpers/Hooks/useApiQuery'; @@ -56,13 +57,9 @@ export const useAddTag = (onTagCreated?: (tag: Tag) => void) => { setError(null); }, onSuccess: (data) => { - queryClient.setQueryData(['tag'], (oldData) => { - if (!oldData) { - return oldData; - } - - return [...oldData, data]; - }); + queryClient.setQueryData(['tag'], (oldData = []) => + addOrUpdateQueryClientItem(oldData, data, 'id') + ); onTagCreated?.(data); }, diff --git a/frontend/src/Utilities/Date/getRelativeDate.ts b/frontend/src/Utilities/Date/getRelativeDate.ts index 1a3d56096..e5ef87bdb 100644 --- a/frontend/src/Utilities/Date/getRelativeDate.ts +++ b/frontend/src/Utilities/Date/getRelativeDate.ts @@ -74,7 +74,7 @@ function getRelativeDate({ if (isInNextWeek(date)) { const dateTime = convertToTimezone(date, timeZone); - const day = dateTime.format('dddd'); + const day = getDayOfWeek(dateTime.day()); return includeTime ? translate('DayOfWeekAt', { day, time }) : day; } @@ -88,3 +88,24 @@ function getRelativeDate({ } export default getRelativeDate; + +function getDayOfWeek(dayNumber: number) { + switch (dayNumber) { + case 0: + return translate('Sunday'); + case 1: + return translate('Monday'); + case 2: + return translate('Tuesday'); + case 3: + return translate('Wednesday'); + case 4: + return translate('Thursday'); + case 5: + return translate('Friday'); + case 6: + return translate('Saturday'); + default: + return ''; + } +} diff --git a/frontend/src/Utilities/String/getLanguageName.ts b/frontend/src/Utilities/String/getLanguageName.ts deleted file mode 100644 index 6bbaf3252..000000000 --- a/frontend/src/Utilities/String/getLanguageName.ts +++ /dev/null @@ -1,41 +0,0 @@ -import createAjaxRequest from 'Utilities/createAjaxRequest'; - -interface LanguageResponse { - identifier: string; -} - -function getLanguage() { - return createAjaxRequest({ - global: false, - dataType: 'json', - url: '/localization/language', - }).request; -} - -function getDisplayName(code: string) { - return Intl.DisplayNames - ? new Intl.DisplayNames([code], { type: 'language' }) - : null; -} - -let languageNames = getDisplayName('en'); - -getLanguage().then((data: LanguageResponse) => { - const names = getDisplayName(data.identifier); - - if (names) { - languageNames = names; - } -}); - -export default function getLanguageName(code: string) { - if (!languageNames) { - return code; - } - - try { - return languageNames.of(code) ?? code; - } catch { - return code; - } -} diff --git a/frontend/src/Wanted/Missing/Missing.tsx b/frontend/src/Wanted/Missing/Missing.tsx index 0e7996601..fdf5d4aa4 100644 --- a/frontend/src/Wanted/Missing/Missing.tsx +++ b/frontend/src/Wanted/Missing/Missing.tsx @@ -20,6 +20,7 @@ import TablePager from 'Components/Table/TablePager'; import Episode from 'Episode/Episode'; import { useToggleEpisodesMonitored } from 'Episode/useEpisode'; import { Filter } from 'Filters/Filter'; +import { useCustomFiltersList } from 'Filters/useCustomFilters'; import { align, icons, kinds } from 'Helpers/Props'; import { SortDirection } from 'Helpers/Props/sortDirections'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; @@ -32,6 +33,7 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; +import MissingFilterModal from './MissingFilterModal'; import { setMissingOption, setMissingOptions, @@ -39,7 +41,7 @@ import { useMissingOptions, } from './missingOptionsStore'; import MissingRow from './MissingRow'; -import useMissing, { FILTERS } from './useMissing'; +import useMissing, { FILTERS, useFilters } from './useMissing'; function getMonitoredValue( filters: Filter[], @@ -66,6 +68,9 @@ function MissingContent() { const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } = useMissingOptions(); + const filters = useFilters(); + const customFilters = useCustomFiltersList('wanted.missing'); + const isSearchingForAllEpisodes = useCommandExecuting( CommandNames.MissingEpisodeSearch ); @@ -257,8 +262,9 @@ function MissingContent() { diff --git a/frontend/src/Wanted/Missing/MissingFilterModal.tsx b/frontend/src/Wanted/Missing/MissingFilterModal.tsx new file mode 100644 index 000000000..f8546bcca --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingFilterModal.tsx @@ -0,0 +1,26 @@ +import React, { useCallback } from 'react'; +import { SetFilter } from 'Components/Filter/Filter'; +import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal'; +import Episode from 'Episode/Episode'; +import { setMissingOption } from './missingOptionsStore'; +import useMissing, { FILTER_BUILDER } from './useMissing'; + +type MissingFilterModalProps = FilterModalProps; + +export default function MissingFilterModal(props: MissingFilterModalProps) { + const { records } = useMissing(); + + const dispatchSetFilter = useCallback(({ selectedFilterKey }: SetFilter) => { + setMissingOption('selectedFilterKey', selectedFilterKey); + }, []); + + return ( + + ); +} diff --git a/frontend/src/Wanted/Missing/useMissing.tsx b/frontend/src/Wanted/Missing/useMissing.tsx index 1a4b2deae..d98bf6a30 100644 --- a/frontend/src/Wanted/Missing/useMissing.tsx +++ b/frontend/src/Wanted/Missing/useMissing.tsx @@ -1,10 +1,13 @@ import { keepPreviousData } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import Episode from 'Episode/Episode'; import { setEpisodeQueryKey } from 'Episode/useEpisode'; -import { Filter } from 'Filters/Filter'; +import { Filter, FilterBuilderProp } from 'Filters/Filter'; +import { useCustomFiltersList } from 'Filters/useCustomFilters'; import usePage from 'Helpers/Hooks/usePage'; import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery'; +import { filterBuilderValueTypes } from 'Helpers/Props'; +import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; import translate from 'Utilities/String/translate'; import { useMissingOptions } from './missingOptionsStore'; @@ -31,20 +34,49 @@ export const FILTERS: Filter[] = [ }, ], }, + { + key: 'excludeSpecials', + label: () => translate('ExcludeSpecials'), + filters: [ + { + key: 'includeSpecials', + value: [false], + type: 'equal', + }, + ], + }, +]; + +export const FILTER_BUILDER: FilterBuilderProp[] = [ + { + name: 'monitored', + label: () => translate('Monitored'), + type: 'exact', + valueType: filterBuilderValueTypes.BOOL, + }, + { + name: 'includeSpecials', + label: () => translate('IncludeSpecials'), + type: 'equal', + valueType: filterBuilderValueTypes.BOOL, + }, ]; const useMissing = () => { const { page, goToPage } = usePage('missing'); const { pageSize, selectedFilterKey, sortKey, sortDirection } = useMissingOptions(); + const customFilters = useCustomFiltersList('wanted.missing'); + + const filters = useMemo(() => { + return findSelectedFilters(selectedFilterKey, FILTERS, customFilters); + }, [selectedFilterKey, customFilters]); const { isPlaceholderData, queryKey, ...query } = usePagedApiQuery({ path: '/wanted/missing', page, pageSize, - queryParams: { - monitored: selectedFilterKey === 'monitored', - }, + filters, sortKey, sortDirection, queryOptions: { @@ -67,3 +99,7 @@ const useMissing = () => { }; export default useMissing; + +export const useFilters = () => { + return FILTERS; +}; diff --git a/frontend/src/polyfills.js b/frontend/src/polyfills.js index 74105388f..6f2a5e74d 100644 --- a/frontend/src/polyfills.js +++ b/frontend/src/polyfills.js @@ -47,3 +47,5 @@ if (!('contains' in String.prototype)) { if (!Object.groupBy) { import('core-js/actual/object/group-by'); } + +import 'core-js/actual/iterator'; diff --git a/frontend/src/typings/ImportListExclusion.ts b/frontend/src/typings/ImportListExclusion.ts deleted file mode 100644 index ec9add4dd..000000000 --- a/frontend/src/typings/ImportListExclusion.ts +++ /dev/null @@ -1,6 +0,0 @@ -import ModelBase from 'App/ModelBase'; - -export default interface ImportListExclusion extends ModelBase { - tvdbId: number; - title: string; -} diff --git a/frontend/src/typings/Indexer.ts b/frontend/src/typings/Indexer.ts deleted file mode 100644 index db0be8531..000000000 --- a/frontend/src/typings/Indexer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import DownloadProtocol from 'DownloadClient/DownloadProtocol'; -import Provider from './Provider'; - -interface Indexer extends Provider { - enableRss: boolean; - enableAutomaticSearch: boolean; - enableInteractiveSearch: boolean; - supportsRss: boolean; - supportsSearch: boolean; - seasonSearchMaximumSingleEpisodeAge: number; - protocol: DownloadProtocol; - priority: number; - downloadClientId: number; - tags: number[]; -} - -export default Indexer; diff --git a/frontend/src/typings/IndexerFlag.ts b/frontend/src/typings/IndexerFlag.ts deleted file mode 100644 index 2c7d97a73..000000000 --- a/frontend/src/typings/IndexerFlag.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface IndexerFlag { - id: number; - name: string; -} - -export default IndexerFlag; diff --git a/frontend/src/typings/Queue.ts b/frontend/src/typings/Queue.ts index 885ceaa7d..b43a6adc7 100644 --- a/frontend/src/typings/Queue.ts +++ b/frontend/src/typings/Queue.ts @@ -42,15 +42,13 @@ interface Queue extends ModelBase { protocol: DownloadProtocol; downloadClient: string; outputPath: string; - episodeHasFile: boolean; + episodesWithFilesCount: number; seriesId?: number; - episodeId?: number; episodeIds: number[]; - seasonNumber?: number; seasonNumbers: number[]; downloadClientHasPostImportCategory: boolean; isFullSeason: boolean; - episode?: Episode; + episodes?: Episode[]; } export default Queue; diff --git a/frontend/src/typings/Settings/IndexerOptions.ts b/frontend/src/typings/Settings/IndexerOptions.ts deleted file mode 100644 index 1eb21de6e..000000000 --- a/frontend/src/typings/Settings/IndexerOptions.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface IndexerOptions { - minimumAge: number; - retention: number; - maximumSize: number; - rssSyncInterval: number; -} diff --git a/global.json b/global.json index 058bafa62..d20023b35 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.103" + "version": "10.0.203" } } diff --git a/package.json b/package.json index e015d686a..4eff18a48 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,14 @@ "history": "4.10.1", "jdu": "1.0.0", "jquery": "3.7.1", - "lodash": "4.17.21", + "lodash": "4.17.23", "mobile-detect": "1.4.5", "moment": "2.30.1", "moment-timezone": "0.6.0", "mousetrap": "1.6.5", "normalize.css": "8.0.1", "prop-types": "15.8.1", - "qs": "6.13.0", + "qs": "6.15.0", "rdndmb-html5-to-touch": "8.1.2", "react": "18.3.1", "react-addons-shallow-compare": "15.6.3", @@ -94,7 +94,7 @@ "@babel/preset-env": "7.28.5", "@babel/preset-react": "7.28.5", "@babel/preset-typescript": "7.28.5", - "@types/lodash": "4.14.195", + "@types/lodash": "4.14.202", "@types/moment-timezone": "0.5.30", "@types/mousetrap": "1.6.15", "@types/qs": "6.9.16", @@ -128,7 +128,7 @@ "file-loader": "6.2.0", "filemanager-webpack-plugin": "8.0.0", "fork-ts-checker-webpack-plugin": "8.0.0", - "html-webpack-plugin": "5.6.0", + "html-webpack-plugin": "5.6.6", "loader-utils": "^3.2.1", "mini-css-extract-plugin": "2.9.1", "postcss": "8.4.47", @@ -137,18 +137,17 @@ "postcss-mixins": "9.0.4", "postcss-nested": "6.2.0", "postcss-simple-vars": "7.0.1", - "postcss-url": "10.1.3", "prettier": "2.8.8", "require-nocache": "1.0.0", - "rimraf": "6.1.2", + "rimraf": "6.1.3", "style-loader": "3.3.2", "stylelint": "15.6.1", "stylelint-order": "6.0.4", - "terser-webpack-plugin": "5.3.10", + "terser-webpack-plugin": "5.3.17", "ts-loader": "9.5.1", "typescript-plugin-css-modules": "5.0.1", "url-loader": "4.1.1", - "webpack": "5.95.0", + "webpack": "5.105.2", "webpack-cli": "5.1.4", "webpack-livereload-plugin": "3.0.2", "worker-loader": "3.0.8" diff --git a/scripts/docs.sh b/scripts/docs.sh index 409a71f98..52b8ca815 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -38,7 +38,7 @@ 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 10.1.0 Swashbuckle.AspNetCore.Cli +dotnet tool install --version 10.1.7 Swashbuckle.AspNetCore.Cli # Remove the openapi.json file so we can check if it was created rm $outputFile diff --git a/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs index c51ab7ad4..167899efc 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs @@ -17,6 +17,8 @@ public class NumberExtensionFixture [TestCase(-1000000, "-976.6 KB")] [TestCase(-377487360, "-360.0 MB")] [TestCase(-1255864686, "-1.2 GB")] + [TestCase(long.MinValue, "-8.0 EB")] + [TestCase(long.MaxValue, "8.0 EB")] public void should_calculate_string_correctly(long bytes, string expected) { bytes.SizeSuffix().Should().Be(expected); diff --git a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs index 6eb544c1d..45dbcd1d6 100644 --- a/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs +++ b/src/NzbDrone.Common/Extensions/IEnumerableExtensions.cs @@ -78,7 +78,8 @@ public static Dictionary ToDictionaryIgnoreDuplicates(this List source, TSource item) + #nullable enable + public static void AddIfNotNull(this List source, TSource? item) { if (item == null) { @@ -87,6 +88,7 @@ public static void AddIfNotNull(this List source, TSource item source.Add(item); } + #nullable disable public static bool Empty(this IEnumerable source) { diff --git a/src/NzbDrone.Common/Extensions/NumberExtensions.cs b/src/NzbDrone.Common/Extensions/NumberExtensions.cs index 15037b20b..efa8b7fc1 100644 --- a/src/NzbDrone.Common/Extensions/NumberExtensions.cs +++ b/src/NzbDrone.Common/Extensions/NumberExtensions.cs @@ -11,16 +11,21 @@ public static string SizeSuffix(this long bytes) { const int bytesInKb = 1024; - if (bytes < 0) - { - return "-" + SizeSuffix(-bytes); - } - if (bytes == 0) { return "0 B"; } + if (bytes == long.MinValue) + { + return "-" + SizeSuffix(long.MaxValue); + } + + if (bytes < 0) + { + return "-" + SizeSuffix(Math.Abs(bytes)); + } + var mag = (int)Math.Log(bytes, bytesInKb); var adjustedSize = bytes / (decimal)Math.Pow(bytesInKb, mag); diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 8a4d140f7..1ec592515 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; +using Diacritical; +using NzbDrone.Common.Globalization; namespace NzbDrone.Common.Extensions { @@ -13,6 +14,11 @@ public static class StringExtensions { private static readonly Regex CamelCaseRegex = new Regex("(? Provide() + { + return new Dictionary + { + { 'ð', "d" }, + { 'Ð', "D" }, + { 'þ', "th" }, + }; + } +} diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 147e5e497..4f7298a2a 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -4,22 +4,23 @@ ISMUSL + - - + + - + - + - - + + - + diff --git a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs index e4671f91b..9c0bf09b4 100644 --- a/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentation/Scene/SceneMappingServiceFixture.cs @@ -46,6 +46,10 @@ public void Setup() _provider2 = new Mock(); _provider2.Setup(s => s.GetSceneMappings()).Returns(_fakeMappings); + + Mocker.GetMock() + .Setup(c => c.GetAllByType(It.IsAny())) + .Returns(new List()); } private void GivenProviders(IEnumerable> providers) @@ -375,15 +379,15 @@ public void should_pick_best_season() private void AssertNoUpdate() { _provider1.Verify(c => c.GetSceneMappings(), Times.Once()); - Mocker.GetMock().Verify(c => c.Clear(It.IsAny()), Times.Never()); - Mocker.GetMock().Verify(c => c.InsertMany(_fakeMappings), Times.Never()); + Mocker.GetMock().Verify(c => c.InsertMany(It.IsAny>()), Times.Never()); + Mocker.GetMock().Verify(c => c.UpdateMany(It.IsAny>()), Times.Never()); + Mocker.GetMock().Verify(c => c.DeleteMany(It.IsAny>()), Times.Never()); } private void AssertMappingUpdated() { _provider1.Verify(c => c.GetSceneMappings(), Times.Once()); - Mocker.GetMock().Verify(c => c.Clear(It.IsAny()), Times.Once()); - Mocker.GetMock().Verify(c => c.InsertMany(_fakeMappings), Times.Once()); + Mocker.GetMock().Verify(c => c.InsertMany(It.IsAny>()), Times.Once()); foreach (var sceneMapping in _fakeMappings) { diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/225_mediainfo_multiple_streamsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/225_mediainfo_multiple_streamsFixture.cs index 180144d62..bf528e938 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/225_mediainfo_multiple_streamsFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/225_mediainfo_multiple_streamsFixture.cs @@ -97,6 +97,63 @@ public void should_convert_non_empty_media_info() mediainfo.SubtitleStreams.Select(s => s.Language).Should().BeEquivalentTo("eng", "ger", "rum"); } + [Test] + public void should_convert_non_empty_media_info_with_empty_audio_languages() + { + 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, + OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]", + MediaInfo = new + { + AudioFormat = "truehd", + AudioCodecID = "[0][0][0][0]", + AudioProfile = "Dolby TrueHD + Dolby Atmos", + AudioBitrate = 224000, + AudioChannels = 2, + AudioChannelPositions = "stereo", + AudioLanguages = new List(), + Subtitles = new List { "ger", "eng", "rum" }, + ScanType = "Progressive", + SchemaRevision = 13 + }.ToJson() + }); + }); + + var items = db.Query("SELECT \"Id\", \"RelativePath\", \"MediaInfo\" FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + var mediainfo = items.First().MediaInfo; + + mediainfo.AudioFormat.Should().BeNull(); + mediainfo.AudioCodecID.Should().BeNull(); + mediainfo.AudioProfile.Should().BeNull(); + mediainfo.AudioBitrate.Should().BeNull(); + mediainfo.AudioChannels.Should().BeNull(); + mediainfo.AudioChannelPositions.Should().BeNull(); + + mediainfo.AudioStreams.First().Format.Should().Be("truehd"); + mediainfo.AudioStreams.First().CodecId.Should().Be("[0][0][0][0]"); + mediainfo.AudioStreams.First().Profile.Should().Be("Dolby TrueHD + Dolby Atmos"); + mediainfo.AudioStreams.First().Bitrate.Should().Be(224000); + mediainfo.AudioStreams.First().Channels.Should().Be(2); + mediainfo.AudioStreams.First().ChannelPositions.Should().Be("stereo"); + mediainfo.AudioStreams.First().Language.Should().Be("und"); + + mediainfo.AudioStreams.Select(s => s.Language).Should().BeEquivalentTo("und"); + mediainfo.SubtitleStreams.Select(s => s.Language).Should().BeEquivalentTo("eng", "ger", "rum"); + } + [Test] public void should_convert_to_null_on_invalid_media_info() { diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AirDateSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AirDateSpecificationFixture.cs new file mode 100644 index 000000000..8190ea853 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AirDateSpecificationFixture.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + public class AirDateSpecificationFixture : CoreTest + { + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _remoteEpisode = new RemoteEpisode + { + Series = new Series + { + Tags = new HashSet() + }, + Episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.AirDateUtc = DateTime.UtcNow) + .Build() + .ToList(), + Release = new ReleaseInfo + { + PublishDate = DateTime.UtcNow.AddDays(-1) + } + }; + } + + private void GivenSettings(bool airDateRestriction, int gracePeriod) + { + Mocker.GetMock() + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) + .Returns(new List + { + new() + { + AirDateRestriction = airDateRestriction, + AirDateGracePeriod = gracePeriod + } + }); + } + + [Test] + public void should_be_true_if_profile_does_not_enforce_air_date_restriction() + { + GivenSettings(false, 0); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_true_if_release_date_is_after_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1); + + GivenSettings(true, 0); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_true_if_release_date_with_grace_period_is_after_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1); + + GivenSettings(true, -2); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_true_if_release_date_is_the_same_as_air_date() + { + var airDate = DateTime.UtcNow; + _remoteEpisode.Episodes.First().AirDateUtc = airDate; + _remoteEpisode.Release.PublishDate = airDate; + + GivenSettings(true, 0); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_false_if_air_date_is_null() + { + _remoteEpisode.Episodes.First().AirDateUtc = null; + + GivenSettings(true, -2); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_false_if_release_date_is_before_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1); + + GivenSettings(true, 0); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_false_if_release_date_with_grace_period_is_before_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-3); + + GivenSettings(true, -2); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_false_if_release_date_is_after_air_date_and_grace_period_is_positive() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1); + + GivenSettings(true, 2); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_false_if_release_date_with_highest_grace_period_is_before_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1); + + Mocker.GetMock() + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) + .Returns(new List + { + new() + { + AirDateRestriction = true, + AirDateGracePeriod = 0 + }, + new() + { + AirDateRestriction = true, + AirDateGracePeriod = -5 + } + }); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_false_if_one_release_profile_does_not_allow_grabbing_before_air_date() + { + _remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow; + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1); + + Mocker.GetMock() + .Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny())) + .Returns(new List + { + new() + { + AirDateRestriction = true, + AirDateGracePeriod = 0 + }, + new() + { + AirDateRestriction = false, + AirDateGracePeriod = 0 + } + }); + + Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs index 1ee16e1d1..99f9a6f80 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs @@ -44,10 +44,7 @@ public void should_return_true_if_it_is_not_a_repack() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeTrue(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeTrue(); } [Test] @@ -60,10 +57,7 @@ public void should_return_true_if_there_are_is_no_episode_file() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeTrue(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeTrue(); } [Test] @@ -81,10 +75,7 @@ public void should_return_true_if_is_a_repack_for_a_different_quality() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeTrue(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeTrue(); } [Test] @@ -102,10 +93,7 @@ public void should_return_true_if_is_a_repack_for_existing_file() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeTrue(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeTrue(); } [Test] @@ -123,10 +111,7 @@ public void should_return_false_if_is_a_repack_for_a_different_file() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeFalse(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeFalse(); } [Test] @@ -144,10 +129,7 @@ public void should_return_false_if_release_group_for_existing_file_is_unknown() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeFalse(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeFalse(); } [Test] @@ -167,10 +149,7 @@ public void should_return_false_if_release_group_for_release_is_unknown() .With(e => e.Episodes = _episodes) .Build(); - Subject.IsSatisfiedBy(remoteEpisode, null) - .Accepted - .Should() - .BeFalse(); + Subject.IsSatisfiedBy(remoteEpisode, new()).Accepted.Should().BeFalse(); } [Test] diff --git a/src/NzbDrone.Core.Test/Files/Indexers/encoded_title.xml b/src/NzbDrone.Core.Test/Files/Indexers/encoded_title.xml new file mode 100644 index 000000000..c0144045d --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/Indexers/encoded_title.xml @@ -0,0 +1,18 @@ + + + + + + + Series.&amp;.Title.S02E19.EAC3.5.1.1080p.WEBRip.x265-iVy + https://my.indexer.com/info.php?guid=abc123 + https://my.indexer.com/api?t=get&id=abc123&apikey=secret + https://my.indexer.com/info.php?guid=abc123 + Fri, 20 Dec 2024 05:16:34 +0000 + TV > HD + Series.&.Title.S02E19.EAC3.5.1.1080p.WEBRip.x265-iVy + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/FluentTest.cs b/src/NzbDrone.Core.Test/FluentTest.cs index 30a3b1ab9..11b628273 100644 --- a/src/NzbDrone.Core.Test/FluentTest.cs +++ b/src/NzbDrone.Core.Test/FluentTest.cs @@ -175,7 +175,9 @@ public void MinOrDefault_should_return_zero_when_collection_is_null() [TestCase(199, 100, 100)] [TestCase(1000, 100, 1000)] [TestCase(0, 100, 0)] - public void round_to_level(long number, int level, int result) + [TestCase(long.MinValue, 1000, -9223372036854775000L)] + [TestCase(long.MaxValue, 1000, 9223372036854775000L)] + public void round_to_level(long number, int level, long result) { number.Round(level).Should().Be(result); } diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs index 0da5fb02a..fbbe4502b 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/ReleaseSearchServiceFixture.cs @@ -331,6 +331,21 @@ public async Task scene_seasonsearch_should_use_seasonnumber_if_no_scene_number_ criteria[0].SeasonNumber.Should().Be(7); } + [Test] + public async Task scene_seasonsearch_should_skip_search_if_no_episodes_after_filtering() + { + WithEpisodes(); + _xemEpisodes.ForEach(e => e.EpisodeFileId = 1); + + var allCriteria = WatchForSearchCriteria(); + + await Subject.SeasonSearch(_xemSeries.Id, 1, true, false, true, false); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(0); + } + [Test] public async Task season_search_for_anime_should_search_for_each_monitored_episode() { diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs index ab6ab0755..774cc7d1f 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs @@ -1,13 +1,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -25,7 +28,8 @@ public void Setup() { Id = 1, Title = "Title", - Seasons = new List() + Seasons = new List(), + QualityProfile = new LazyLoaded(Builder.CreateNew().With(q => q.UpgradeAllowed = true).Build()) }; Mocker.GetMock() @@ -56,6 +60,23 @@ public void should_only_include_monitored_seasons() .Verify(v => v.SeasonSearch(_series.Id, It.IsAny(), false, true, true, false), Times.Exactly(_series.Seasons.Count(s => s.Monitored))); } + [Test] + public void should_only_search_missing_if_profile_does_not_allow_upgrades() + { + _series.Seasons = new List + { + new Season { SeasonNumber = 0, Monitored = false }, + new Season { SeasonNumber = 1, Monitored = true } + }; + + _series.QualityProfile.Value.UpgradeAllowed = false; + + Subject.Execute(new SeriesSearchCommand { SeriesId = _series.Id, Trigger = CommandTrigger.Manual }); + + Mocker.GetMock() + .Verify(v => v.SeasonSearch(_series.Id, It.IsAny(), true, true, true, false), Times.Exactly(_series.Seasons.Count(s => s.Monitored))); + } + [Test] public void should_start_with_lower_seasons_first() { diff --git a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs index b2e171a58..bf4695826 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs @@ -58,5 +58,16 @@ public void should_handle_relative_url() result.First().CommentUrl.Should().Be("http://my.indexer.com/details/123#comments"); result.First().DownloadUrl.Should().Be("http://my.indexer.com/getnzb/123.nzb&i=782&r=123"); } + + [Test] + public void should_decode_html_entities_in_item_title() + { + var xml = ReadAllText("Files/Indexers/encoded_title.xml"); + + var result = Subject.ParseResponse(CreateResponse("http://my.indexer.com/rss", xml)); + + result.Should().HaveCount(1); + result.First().Title.Should().Be("Series.&.Title.S02E19.EAC3.5.1.1080p.WEBRip.x265-iVy"); + } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 0dce58278..82e07a190 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -80,7 +80,7 @@ private void GivenSuccessfulImport() imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -124,7 +124,7 @@ public void should_skip_if_no_series_found() Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); Mocker.GetMock() - .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), true), + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), true), Times.Never()); VerifyNoImport(); @@ -175,7 +175,7 @@ public void should_not_delete_folder_if_files_were_imported_and_video_files_rema imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -201,7 +201,7 @@ public void should_delete_folder_if_files_were_imported_and_only_sample_files_re imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -271,7 +271,7 @@ public void should_not_delete_if_there_is_large_rar_file() imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -322,7 +322,7 @@ public void should_use_folder_if_folder_import() Subject.ProcessPath(fileName); Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.Is(v => v.AbsoluteEpisodeNumbers.First() == 9), true), Times.Once()); + .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.Is(v => v.AbsoluteEpisodeNumbers.First() == 9), true), Times.Once()); } [Test] @@ -346,7 +346,7 @@ public void should_not_use_folder_if_file_import() var result = Subject.ProcessPath(fileName); Mocker.GetMock() - .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true), Times.Once()); + .Verify(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true), Times.Once()); } [Test] @@ -379,7 +379,7 @@ public void should_not_delete_if_no_files_were_imported() imported.Add(new ImportDecision(localEpisode)); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -456,7 +456,7 @@ public void should_return_rejection_if_nothing_imported_and_contains_rar_file() var imported = new List(); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -482,7 +482,7 @@ public void should_return_rejection_if_nothing_imported_and_contains_executable_ var imported = new List(); Mocker.GetMock() - .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), null, true, true)) + .Setup(s => s.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), null, true, true)) .Returns(imported); Mocker.GetMock() @@ -499,6 +499,33 @@ public void should_return_rejection_if_nothing_imported_and_contains_executable_ result.First().Result.Should().Be(ImportResultType.Rejected); } + [Test] + public void should_reject_if_download_is_multi_season() + { + GivenValidSeries(); + + _trackedDownload.DownloadItem.Title = "Series Title S01-S11"; + + var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic(); + + Mocker.GetMock().Setup(c => c.FolderExists(folderName)) + .Returns(true); + + var result = Subject.ProcessPath(folderName, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem); + + result.Count.Should().Be(1); + result.First().Result.Should().Be(ImportResultType.Rejected); + result.First().ImportDecision.Rejections.First().Reason.Should().Be(ImportRejectionReason.MultiSeason); + + Mocker.GetMock().Setup(c => c.GetSeries("foldername")).Returns((Series)null); + + Mocker.GetMock() + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), true), + Times.Never()); + + VerifyNoImport(); + } + private void VerifyNoImport() { Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null, ImportMode.Auto), diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 51d181abe..783afd001 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -103,7 +103,7 @@ public void should_call_all_specifications() GivenAugmentationSuccess(); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); - Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, false, true); + Subject.GetImportDecisions(_videoFiles, _series, downloadClientItem, null, null, false, true); _fail1.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(It.IsAny(), downloadClientItem), Times.Once()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationServiceFixture.cs new file mode 100644 index 000000000..3a5ac8ea2 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationServiceFixture.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport +{ + [TestFixture] + public class LocalEpisodeCustomFormatCalculationServiceFixture : CoreTest + { + private const int EnglishCustomFormatScore = 10; + private const int SpanishCustomFormatScore = 2; + private LocalEpisode _localEpisode; + private Series _series; + private QualityModel _quality; + private CustomFormat _englishCustomFormat; + private CustomFormat _spanishCustomFormat; + + [SetUp] + public void Setup() + { + _englishCustomFormat = new CustomFormat("HasEnglish") { Id = 1 }; + _spanishCustomFormat = new CustomFormat("HasSpanish") { Id = 2 }; + _series = Builder.CreateNew() + .With(e => e.Path = @"C:\Test\Series".AsOsAgnostic()) + .With(e => e.QualityProfile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + FormatItems = [ + new ProfileFormatItem { Format = _englishCustomFormat, Score = EnglishCustomFormatScore }, + new ProfileFormatItem { Format = _spanishCustomFormat, Score = SpanishCustomFormatScore } + ] + }) + .Build(); + + _quality = new QualityModel(Quality.DVD); + + _localEpisode = new LocalEpisode + { + Series = _series, + Quality = _quality, + Languages = new List { Language.Spanish }, + Episodes = new List { new Episode() }, + Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi" + }; + + Mocker.GetMock() + .Setup(s => s.ParseCustomFormat(It.IsAny(), It.Is(x => x.Contains("English")))) + .Returns([_englishCustomFormat]); + + Mocker.GetMock() + .Setup(s => s.ParseCustomFormat(It.IsAny(), It.Is(x => x.Contains("Spanish")))) + .Returns([_spanishCustomFormat]); + } + + [Test] + public void should_build_a_filename_and_use_it_to_calculate_custom_score() + { + var renamedFileName = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.English.XviD-OSiTV.avi"; + + Mocker.GetMock() + .Setup(s => s.BuildFileName(It.IsAny>(), It.IsAny(), It.IsAny(), "", null, null)) + .Returns(renamedFileName); + + Subject.ParseEpisodeCustomFormats(_localEpisode).Should().BeEquivalentTo([_englishCustomFormat]); + } + + [Test] + public void should_update_custom_formats_on_local_episode() + { + var renamedFileName = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.English.XviD-OSiTV.avi"; + + Mocker.GetMock() + .Setup(s => s.BuildFileName(It.IsAny>(), It.IsAny(), It.IsAny(), "", null, null)) + .Returns(renamedFileName); + + Subject.UpdateEpisodeCustomFormats(_localEpisode); + _localEpisode.FileNameUsedForCustomFormatCalculation.Should().Be(renamedFileName); + + _localEpisode.OriginalFileNameCustomFormats.Should().BeEquivalentTo([_spanishCustomFormat]); + _localEpisode.OriginalFileNameCustomFormatScore.Should().Be(SpanishCustomFormatScore); + + _localEpisode.CustomFormats.Should().BeEquivalentTo([_englishCustomFormat]); + _localEpisode.CustomFormatScore.Should().Be(EnglishCustomFormatScore); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index fbad49de8..eaa888733 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles; @@ -565,5 +566,48 @@ public void should_return_false_if_not_upgrade_to_custom_format_score() Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse(); } + + [Test] + public void should_return_false_and_a_specific_reason_if_not_upgrade_to_custom_format_score_after_local_file_rename_but_was_before() + { + var episodeFileCustomFormats = Builder.CreateListOfSize(1).Build().ToList(); + + var episodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.Bluray1080p) + }; + + _series.QualityProfile.Value.FormatItems = episodeFileCustomFormats.Select(c => new ProfileFormatItem + { + Format = c, + Score = 50 + }) + .ToList(); + + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); + + Mocker.GetMock() + .Setup(s => s.ParseCustomFormat(episodeFile)) + .Returns(episodeFileCustomFormats); + + _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); + _localEpisode.CustomFormats = Builder.CreateListOfSize(1).Build().ToList(); + _localEpisode.CustomFormatScore = 20; + _localEpisode.OriginalFileNameCustomFormats = Builder.CreateListOfSize(1).Build().ToList(); + _localEpisode.OriginalFileNameCustomFormatScore = 60; + + _localEpisode.Episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.EpisodeFileId = 1) + .With(e => e.EpisodeFile = new LazyLoaded(episodeFile)) + .Build() + .ToList(); + + var result = Subject.IsSatisfiedBy(_localEpisode, null); + result.Accepted.Should().BeFalse(); + result.Reason.Should().Be(ImportRejectionReason.NotCustomFormatUpgradeAfterRename); + } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs index 6bbd36434..2bec1811e 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileDeletionService/DeleteEpisodeFileFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -41,6 +42,10 @@ public void Setup() private void GivenRootFolderExists() { + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(_series.Path)) + .Returns(ROOT_FOLDER); + Mocker.GetMock() .Setup(s => s.FolderExists(ROOT_FOLDER)) .Returns(true); diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index f573270c2..149208a3e 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -168,6 +168,8 @@ public void should_parse_absolute_specials(string postTitle, string title, int a } [TestCase("[Underwater] Another OVA - The Other -Karma- (BD 1080p) [3A561D0E].mkv", "Another", 0)] + [TestCase("[sam] Long Series - NCOP [BD 1080p FLAC] [BBC3BC68].mkv", "Long Series", 0)] + [TestCase("[sam] Long Series - NCED [BD 1080p FLAC] [BBC3BC68].mkv", "Long Series", 0)] public void should_parse_absolute_specials_without_absolute_number(string postTitle, string title, int absoluteEpisodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs index 35ae206f3..68e973279 100644 --- a/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/DailyEpisodeParserFixture.cs @@ -36,6 +36,8 @@ public class DailyEpisodeParserFixture : CoreTest [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("Series.Title.13.02.2025.1080i.HDTV.MPA2.0.H.264", "Series Title", 2025, 2, 13)] + [TestCase("Series.2025.09.01.The.170.Million.Pound.Diamond.Scam.1080p.HDTV.H264-DEADPOOL'", "Series", 2025, 9, 1)] + [TestCase("Series.2025.09.01.The.Million.Pound.Diamond.Scam.1080p.HDTV.H264-DEADPOOL'", "Series", 2025, 9, 1)] public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs index 1c47b4fe8..86ce520eb 100644 --- a/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/NormalizeSeriesTitleFixture.cs @@ -26,6 +26,7 @@ public void should_normalize_series_title(string parsedSeriesName, string series [TestCase("24", "24")] [TestCase("Test: Something à Deux", "testsomethingdeux")] [TestCase("Parler à", "parlera")] + [TestCase("Ríkið", "rikid")] public void should_remove_special_characters_and_casing(string dirty, string clean) { var result = dirty.CleanSeriesTitle(); diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs index 5b26d0461..8fd47c8b0 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/MapFixture.cs @@ -293,5 +293,99 @@ public void should_not_use_scene_season_number_from_xem_mapping_if_alias_matches result.MappedSeasonNumber.Should().Be(sceneMapping.SceneSeasonNumber); } + + [Test] + public void should_use_tvdbid_matching_when_alias_without_year_is_found() + { + var alias = "Series Alias"; + + _parsedEpisodeInfo.SeriesTitle = $"{alias} {_series.Year}"; + _parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear = alias; + _parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year; + + Mocker.GetMock() + .Setup(s => s.FindTvdbId(alias, It.IsAny(), It.IsAny())) + .Returns(_series.TvdbId); + + Mocker.GetMock() + .Setup(s => s.FindByTvdbId(_series.Id)) + .Returns(_series); + + var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, null); + + result.Series.Should().Be(_series); + + Mocker.GetMock() + .Verify(v => v.FindByTvdbId(It.IsAny()), Times.Once()); + } + + [Test] + public void should_not_use_tvdbid_matching_when_alias_without_year_is_found_with_wrong_year() + { + var alias = "Series Alias"; + + _parsedEpisodeInfo.SeriesTitle = $"{alias} {_series.Year}"; + _parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear = alias; + _parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year + 1; + + Mocker.GetMock() + .Setup(s => s.FindTvdbId(alias, It.IsAny(), It.IsAny())) + .Returns(_series.TvdbId); + + Mocker.GetMock() + .Setup(s => s.FindByTvdbId(_series.Id)) + .Returns(_series); + + var result = Subject.Map(_parsedEpisodeInfo, 0, 0, "", null); + + result.Series.Should().BeNull(); + + Mocker.GetMock() + .Verify(v => v.FindByTvdbId(It.IsAny()), Times.Once()); + } + + [Test] + public void should_use_year_when_looking_up_by_all_titles_in_release_title() + { + var alias = "Series Alias"; + var title = "Series Title"; + + _parsedEpisodeInfo.SeriesTitle = $"Series Title AKA Series Alias {_series.Year}"; + _parsedEpisodeInfo.SeriesTitleInfo.AllTitles = [ + title, + alias + ]; + _parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year; + + Mocker.GetMock() + .Setup(s => s.FindByTitle(title, _series.Year)) + .Returns(_series); + + var result = Subject.Map(_parsedEpisodeInfo, 0, 0, "", null); + + result.Series.Should().Be(_series); + } + + [Test] + public void should_use_title_with_year_when_looking_up_by_all_titles_in_release_title() + { + var alias = "Series Alias"; + var title = "Series Title"; + + _parsedEpisodeInfo.SeriesTitle = $"Series Title AKA Series Alias {_series.Year}"; + _parsedEpisodeInfo.SeriesTitleInfo.AllTitles = [ + title, + alias + ]; + _parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year; + + Mocker.GetMock() + .Setup(s => s.FindByTitle($"{title} {_series.Year}")) + .Returns(_series); + + var result = Subject.Map(_parsedEpisodeInfo, 0, 0, "", null); + + result.Series.Should().Be(_series); + } } } diff --git a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj index 56177aa94..f5f9283ae 100644 --- a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj @@ -3,7 +3,7 @@ net10.0 - + @@ -18,5 +18,8 @@ PreserveNewest + + PreserveNewest + diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/OriginalCountrySpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/OriginalCountrySpecification.cs new file mode 100644 index 000000000..b86076801 --- /dev/null +++ b/src/NzbDrone.Core/AutoTagging/Specifications/OriginalCountrySpecification.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.AutoTagging.Specifications +{ + public class OriginalCountrySpecificationValidator : AbstractValidator + { + public OriginalCountrySpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty(); + + RuleFor(c => c.Value).Custom((countries, context) => + { + if (countries.Any(c => c.Length != 3)) + { + context.AddFailure("Country code must be 3 characters long"); + } + }); + } + } + + public class OriginalCountrySpecification : AutoTaggingSpecificationBase + { + private static readonly OriginalCountrySpecificationValidator Validator = new(); + + public override int Order => 1; + public override string ImplementationName => "Original Country"; + + [FieldDefinition(1, Label = "AutoTaggingSpecificationOriginalCountry", Type = FieldType.Tag)] + public IEnumerable Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(Series series) + { + return Value.Any(network => series.OriginalCountry.EqualsIgnoreCase(network)); + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 8cd48849b..1902a9773 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -137,6 +137,7 @@ public void SaveConfigDictionary(Dictionary configValues) _cache.Clear(); var allWithDefaults = GetConfigDictionary(); + var hasUpdated = false; foreach (var configValue in configValues) { @@ -155,11 +156,15 @@ public void SaveConfigDictionary(Dictionary configValues) if (!equal) { + hasUpdated = true; SetValue(configValue.Key.FirstCharToUpper(), configValue.Value.ToString()); } } - _eventAggregator.PublishEvent(new ConfigFileSavedEvent()); + if (hasUpdated) + { + _eventAggregator.PublishEvent(new ConfigFileSavedEvent()); + } } public string BindAddress diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index cf5a35529..72ae79891 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -55,6 +55,7 @@ private Dictionary AllWithDefaults() public void SaveConfigDictionary(Dictionary configValues) { var allWithDefaults = AllWithDefaults(); + var hasUpdated = false; foreach (var configValue in configValues) { @@ -68,11 +69,15 @@ public void SaveConfigDictionary(Dictionary configValues) if (!equal) { + hasUpdated = true; SetValue(configValue.Key, configValue.Value.ToString()); } } - _eventAggregator.PublishEvent(new ConfigSavedEvent()); + if (hasUpdated) + { + _eventAggregator.PublishEvent(new ConfigSavedEvent()); + } } public bool IsDefined(string key) diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index 1f0cb296b..14f206609 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -19,7 +19,7 @@ public interface ICustomFormatCalculationService List ParseCustomFormat(EpisodeFile episodeFile); List ParseCustomFormat(Blocklist blocklist, Series series); List ParseCustomFormat(EpisodeHistory history, Series series); - List ParseCustomFormat(LocalEpisode localEpisode); + List ParseCustomFormat(LocalEpisode localEpisode, string fileName); } public class CustomFormatCalculationService : ICustomFormatCalculationService @@ -114,12 +114,12 @@ public List ParseCustomFormat(EpisodeHistory history, Series serie return ParseCustomFormat(input); } - public List ParseCustomFormat(LocalEpisode localEpisode) + public List ParseCustomFormat(LocalEpisode localEpisode, string fileName) { var episodeInfo = new ParsedEpisodeInfo { SeriesTitle = localEpisode.Series.Title, - ReleaseTitle = localEpisode.SceneName.IsNotNullOrWhiteSpace() ? localEpisode.SceneName : Path.GetFileName(localEpisode.Path), + ReleaseTitle = localEpisode.SceneName.IsNotNullOrWhiteSpace() ? localEpisode.SceneName : fileName, Quality = localEpisode.Quality, Languages = localEpisode.Languages, ReleaseGroup = localEpisode.ReleaseGroup @@ -133,7 +133,7 @@ public List ParseCustomFormat(LocalEpisode localEpisode) Languages = localEpisode.Languages, IndexerFlags = localEpisode.IndexerFlags, ReleaseType = localEpisode.ReleaseType, - Filename = Path.GetFileName(localEpisode.Path) + Filename = fileName }; return ParseCustomFormat(input); diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs index 52cb21160..20c116d65 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene { public class SceneMapping : ModelBase { + public string MappingId { get; set; } public string Title { get; set; } public string ParseTerm { get; set; } diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs index 92119ec55..45d91f362 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene public interface ISceneMappingRepository : IBasicRepository { List FindByTvdbid(int tvdbId); - void Clear(string type); + List GetAllByType(string type); } public class SceneMappingRepository : BasicRepository, ISceneMappingRepository @@ -22,9 +22,9 @@ public List FindByTvdbid(int tvdbId) return Query(x => x.TvdbId == tvdbId); } - public void Clear(string type) + public List GetAllByType(string type) { - Delete(s => s.Type == type); + return Query(x => x.Type == type); } } } diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index b9e568163..e0a145760 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -62,7 +62,6 @@ public List GetSceneNames(int tvdbId, List seasonNumbers, List var names = mappings.Where(n => seasonNumbers.Contains(n.SeasonNumber ?? -1) || 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(StringComparer.InvariantCultureIgnoreCase) .ToList(); @@ -144,7 +143,7 @@ private void UpdateMappings() if (mappings.Any()) { - _repository.Clear(sceneMappingProvider.GetType().Name); + var providerType = sceneMappingProvider.GetType().Name; mappings.RemoveAll(sceneMapping => { @@ -161,10 +160,45 @@ private void UpdateMappings() foreach (var sceneMapping in mappings) { sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle(); - sceneMapping.Type = sceneMappingProvider.GetType().Name; + sceneMapping.Type = providerType; } - _repository.InsertMany(mappings.ToList()); + var existing = _repository.GetAllByType(providerType); + var existingByMappingId = new Dictionary(); + + foreach (var e in existing) + { + existingByMappingId[e.MappingId ?? $"{e.Id}"] = e; + } + + var toInsert = new List(); + var toUpdate = new List(); + + foreach (var mapping in mappings) + { + if (mapping.MappingId.IsNullOrWhiteSpace()) + { + _logger.Warn("Scene mapping with missing MappingId found for: {0} {1}, skipping", mapping.TvdbId, mapping.Title); + continue; + } + + if (existingByMappingId.TryGetValue(mapping.MappingId, out var existingMapping)) + { + mapping.Id = existingMapping.Id; + toUpdate.Add(mapping); + existingByMappingId.Remove(mapping.MappingId); + } + else + { + toInsert.Add(mapping); + } + } + + var toDelete = existingByMappingId.Values.ToList(); + + _repository.DeleteMany(toDelete); + _repository.UpdateMany(toUpdate); + _repository.InsertMany(toInsert); } else { @@ -274,11 +308,6 @@ private List FilterSceneMappings(List candidates, in return normalCandidates; } - private bool IsEnglish(string title) - { - return title.All(c => c <= 255); - } - public void Handle(SeriesRefreshStartingEvent message) { if (message.ManualTrigger && (_findByTvdbIdCache.IsExpired(TimeSpan.FromMinutes(1)) || !_updatedAfterStartup)) diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs index 4ebe2ba4b..86796ae8f 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs @@ -101,6 +101,7 @@ public List GetSceneTvdbNames() result.Add(new SceneMapping { + MappingId = $"x-{series.Key}_S{seasonNumber}_{n.Key.Replace(' ', '_')}", Title = n.Key, SearchTerm = n.Key, SceneSeasonNumber = seasonNumber, diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 59fb6afcd..cae2e8302 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -266,8 +266,10 @@ public void UpdateMany(IList models) } using (var conn = _database.OpenConnection()) + using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { - UpdateFields(conn, null, models, _properties); + UpdateFields(conn, tran, models, _properties); + tran.Commit(); } } @@ -371,8 +373,10 @@ public void SetFields(IList models, params Expression x.GetMemberName()).ToList(); using (var conn = _database.OpenConnection()) + using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { - UpdateFields(conn, null, models, propertiesToUpdate); + UpdateFields(conn, tran, models, propertiesToUpdate); + tran.Commit(); } foreach (var model in models) diff --git a/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs index b7981367f..ca0085032 100644 --- a/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs @@ -34,8 +34,15 @@ public override Language Parse(object value) public class LanguageIntConverter : JsonConverter { + public override bool HandleNull => true; + public override Language Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + { + return Language.Unknown; + } + var item = reader.GetInt32(); return (Language)item; } diff --git a/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs index bb8801a44..7f5bc7ff4 100644 --- a/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs +++ b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs @@ -773,7 +773,7 @@ private List MigrateLanguages(string mediaInfoLanguages) try { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]); + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveDiacritics() == tokens[i]); if (cultureInfo != null) { diff --git a/src/NzbDrone.Core/Datastore/Migration/225_mediainfo_multiple_streams.cs b/src/NzbDrone.Core/Datastore/Migration/225_mediainfo_multiple_streams.cs index 48e616158..b16117241 100644 --- a/src/NzbDrone.Core/Datastore/Migration/225_mediainfo_multiple_streams.cs +++ b/src/NzbDrone.Core/Datastore/Migration/225_mediainfo_multiple_streams.cs @@ -46,8 +46,6 @@ private void MigrateMediaInfoToMultipleStreams(IDbConnection conn, IDbTransactio { var existing = conn.Query("SELECT \"Id\", \"MediaInfo\" FROM \"EpisodeFiles\""); - var updated = new List(); - foreach (var row in existing) { if (row.MediaInfo.IsNullOrWhiteSpace()) @@ -64,7 +62,7 @@ private void MigrateMediaInfoToMultipleStreams(IDbConnection conn, IDbTransactio { _logger.Warn(ex, "Episode {EpisodeId} contains invalid JSON data, skipping.", row.Id); - updated.Add(new EpisodeFile225 { Id = row.Id, MediaInfo = null }); + UpdateMediaInfoForEpisodeFile(conn, tran, new EpisodeFile225 { Id = row.Id, MediaInfo = null }); continue; } @@ -83,17 +81,17 @@ private void MigrateMediaInfoToMultipleStreams(IDbConnection conn, IDbTransactio continue; } - updated.Add(new EpisodeFile225 + UpdateMediaInfoForEpisodeFile(conn, tran, new EpisodeFile225 { Id = row.Id, MediaInfo = JsonSerializer.Serialize(newMediaInfo, _serializerSettings) }); } + } - conn.Execute( - "UPDATE \"EpisodeFiles\" SET \"MediaInfo\" = @MediaInfo WHERE \"Id\" = @Id", - updated, - transaction: tran); + private static void UpdateMediaInfoForEpisodeFile(IDbConnection conn, IDbTransaction tran, EpisodeFile225 updated) + { + conn.Execute("UPDATE \"EpisodeFiles\" SET \"MediaInfo\" = @MediaInfo WHERE \"Id\" = @Id", updated, transaction: tran); } private static MediaInfo225 MigrateMediaInfo(MediaInfo224 old) @@ -128,13 +126,19 @@ private static List MigrateAudioStreams(MediaInfo224 ol { Language = language, }) - .ToList(); - audioStreams?.FirstOrDefault()?.Format = old.AudioFormat; - audioStreams?.FirstOrDefault()?.CodecId = old.AudioCodecID; - audioStreams?.FirstOrDefault()?.Profile = old.AudioProfile; - audioStreams?.FirstOrDefault()?.Bitrate = old.AudioBitrate; - audioStreams?.FirstOrDefault()?.Channels = old.AudioChannels; - audioStreams?.FirstOrDefault()?.ChannelPositions = old.AudioChannelPositions; + .ToList() ?? []; + + if (audioStreams.Count == 0) + { + audioStreams.Add(new MediaInfoAudioStream225 { Language = "und" }); + } + + audioStreams.FirstOrDefault()?.Format = old.AudioFormat; + audioStreams.FirstOrDefault()?.CodecId = old.AudioCodecID; + audioStreams.FirstOrDefault()?.Profile = old.AudioProfile; + audioStreams.FirstOrDefault()?.Bitrate = old.AudioBitrate; + audioStreams.FirstOrDefault()?.Channels = old.AudioChannels; + audioStreams.FirstOrDefault()?.ChannelPositions = old.AudioChannelPositions; return audioStreams; } @@ -146,7 +150,7 @@ private static List MigrateSubtitleStreams(MediaInfo { Language = language, }) - .ToList(); + .ToList() ?? []; return subtitleStreams; } diff --git a/src/NzbDrone.Core/Datastore/Migration/226_add_air_date_filtering_to_release_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/226_add_air_date_filtering_to_release_profiles.cs new file mode 100644 index 000000000..b7889a3ec --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/226_add_air_date_filtering_to_release_profiles.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration; + +[Migration(226)] +public class add_air_date_filtering_to_release_profiles : NzbDroneMigrationBase +{ + protected override void MainDbUpgrade() + { + Alter.Table("ReleaseProfiles").AddColumn("AirDateRestriction").AsBoolean().WithDefaultValue(false); + Alter.Table("ReleaseProfiles").AddColumn("AirDateGracePeriod").AsInt32().WithDefaultValue(0); + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/227_original_country.cs b/src/NzbDrone.Core/Datastore/Migration/227_original_country.cs new file mode 100644 index 000000000..95ee6436a --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/227_original_country.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(227)] + public class original_country : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Series") + .AddColumn("OriginalCountry").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/230_add_mapping_id_to_scene_mappings.cs b/src/NzbDrone.Core/Datastore/Migration/230_add_mapping_id_to_scene_mappings.cs new file mode 100644 index 000000000..991e2f541 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/230_add_mapping_id_to_scene_mappings.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(230)] + public class add_mapping_id_to_scene_mappings : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("SceneMappings") + .AddColumn("MappingId").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs index 5283f33ce..f4e36ed4f 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadRejectionReason.cs @@ -75,5 +75,6 @@ public enum DownloadRejectionReason DiskCustomFormatScore, DiskCustomFormatScoreIncrement, DiskUpgradesNotAllowed, - DiskNotUpgrade + DiskNotUpgrade, + BeforeAirDate } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index e4e25b109..ff275a66c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -89,7 +89,7 @@ public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, ReleaseDecision // If the parsed size is smaller than minSize we don't want it if (subject.Release.Size < minSize) { - var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min"; + var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count} episodes totalling {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 DownloadSpecDecision.Reject(DownloadRejectionReason.BelowMinimumSize, "{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); @@ -110,7 +110,7 @@ public DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, ReleaseDecision // If the parsed size is greater than maxSize we don't want it if (subject.Release.Size > maxSize) { - var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count}x {runtime}min"; + var runtimeMessage = subject.Episodes.Count == 1 ? $"{runtime}min" : $"{subject.Episodes.Count} episodes totalling {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 DownloadSpecDecision.Reject(DownloadRejectionReason.AboveMaximumSize, "{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AirDateSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AirDateSpecification.cs new file mode 100644 index 000000000..cea857b84 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AirDateSpecification.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class AirDateSpecification : IDownloadDecisionEngineSpecification + { + private readonly Logger _logger; + private readonly IReleaseProfileService _releaseProfileService; + private readonly ITermMatcherService _termMatcherService; + + public AirDateSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger) + { + _logger = logger; + _releaseProfileService = releaseProfileService; + _termMatcherService = termMatcherService; + } + + public SpecificationPriority Priority => SpecificationPriority.Database; + public RejectionType Type => RejectionType.Permanent; + + public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, ReleaseDecisionInformation information) + { + _logger.Debug("Checking if release meets air date restrictions: {0}", subject); + + var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Series.Tags, subject.Release.IndexerId); + + if (releaseProfiles.Empty()) + { + _logger.Debug("No Release Profile, accepting"); + return DownloadSpecDecision.Accept(); + } + + var bestProfile = releaseProfiles + .OrderByDescending(p => p.AirDateRestriction ? 1 : 0) + .ThenByDescending(p => p.AirDateGracePeriod) + .First(); + + if (!bestProfile.AirDateRestriction) + { + _logger.Debug("Release Profile does not prevent grabbing before release date, accepting"); + return DownloadSpecDecision.Accept(); + } + + var releaseDate = subject.Release.PublishDate; + var gracePeriod = bestProfile.AirDateGracePeriod; + + foreach (var episode in subject.Episodes) + { + var airDate = episode.AirDateUtc; + + if (!airDate.HasValue) + { + _logger.Debug("No air date available, rejecting"); + return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "No air date available"); + } + + var adjustedAirDate = airDate.Value.AddDays(gracePeriod); + + if (releaseDate < adjustedAirDate) + { + return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "Release date {0} is before adjusted air date of {1} (Air Date: {2}. Grace period {3} days)", releaseDate, adjustedAirDate, airDate, gracePeriod); + } + } + + _logger.Debug("All episodes within air date limitations, allowing"); + return DownloadSpecDecision.Accept(); + } + + private ReleaseProfile FindBestProfile(List releaseProfiles) + { + return releaseProfiles + .OrderBy(p => p.AirDateRestriction ? 0 : 1) + .ThenBy(p => p.AirDateGracePeriod) + .ThenBy(p => p.AirDateRestriction ? 0 : 1) + .FirstOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 6b91b2a63..bdff884d5 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -81,8 +81,8 @@ public override IEnumerable GetItems() { var firstFile = torrent.Files?.FirstOrDefault(); - // skip metadata download - if (firstFile?.Path?.Contains("[METADATA]") == true) + // skip metadata download or if the torrent is already removed + if (firstFile?.Path?.Contains("[METADATA]") == true || torrent.Status == "removed") { continue; } @@ -120,9 +120,6 @@ public override IEnumerable GetItems() case "complete": status = DownloadItemStatus.Completed; break; - case "removed": - status = DownloadItemStatus.Failed; - break; } _logger.Trace($"- aria2 getstatus hash:'{torrent.InfoHash}' gid:'{torrent.Gid}' status:'{status}' total:{totalLength} completed:'{completedLength}'"); @@ -139,7 +136,6 @@ public override IEnumerable GetItems() OutputPath = outputPath, RemainingSize = totalLength - completedLength, RemainingTime = downloadSpeed == 0 ? (TimeSpan?)null : new TimeSpan(0, 0, (int)((totalLength - completedLength) / downloadSpeed)), - Removed = torrent.Status == "removed", SeedRatio = totalLength > 0 ? (double)uploadedLength / totalLength : 0, Status = status, Title = title, diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index ffb7b60be..292009e39 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -172,6 +172,13 @@ public void Import(TrackedDownload trackedDownload) { return; } + + if (firstResult.ImportDecision.Rejections.FirstOrDefault()?.Reason == ImportRejectionReason.MultiSeason) + { + trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, firstResult.Errors)); + SetStateToImportBlocked(trackedDownload); + return; + } } var statusMessages = new List diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 1d98012c4..1906ffef3 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -60,9 +60,9 @@ public DownloadClientDefinition ResolveDownloadClient(int? id, string name) { var all = All(); var clientByName = name.IsNullOrWhiteSpace() ? null : all.FirstOrDefault(c => c.Name.EqualsIgnoreCase(name)); - var clientById = id.HasValue ? all.FirstOrDefault(c => c.Id == id.Value) : null; + var clientById = id is > 0 ? all.FirstOrDefault(c => c.Id == id.Value) : null; - if (id.HasValue && clientById == null) + if (id is > 0 && clientById == null) { throw new ResolveDownloadClientException("Download client with ID '{0}' could not be found", id.Value); } diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 76ed0cb2c..33ae1269d 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -23,7 +23,6 @@ public class DownloadClientItem public bool IsEncrypted { get; set; } public bool CanMoveFiles { get; set; } public bool CanBeRemoved { get; set; } - public bool Removed { get; set; } public DownloadClientItem Clone() { diff --git a/src/NzbDrone.Core/Download/DownloadEventHub.cs b/src/NzbDrone.Core/Download/DownloadEventHub.cs index a1e4a6856..eedd6718b 100644 --- a/src/NzbDrone.Core/Download/DownloadEventHub.cs +++ b/src/NzbDrone.Core/Download/DownloadEventHub.cs @@ -12,14 +12,17 @@ public class DownloadEventHub : IHandle, { private readonly IConfigService _configService; private readonly IProvideDownloadClient _downloadClientProvider; + private readonly ITrackedDownloadService _trackedDownloadService; private readonly Logger _logger; public DownloadEventHub(IConfigService configService, IProvideDownloadClient downloadClientProvider, + ITrackedDownloadService trackedDownloadService, Logger logger) { _configService = configService; _downloadClientProvider = downloadClientProvider; + _trackedDownloadService = trackedDownloadService; _logger = logger; } @@ -28,7 +31,6 @@ public void Handle(DownloadFailedEvent message) var trackedDownload = message.TrackedDownload; if (trackedDownload == null || - message.TrackedDownload.DownloadItem.Removed || !trackedDownload.DownloadItem.CanBeRemoved) { return; @@ -53,8 +55,7 @@ public void Handle(DownloadCompletedEvent message) MarkItemAsImported(trackedDownload, downloadClient); - if (trackedDownload.DownloadItem.Removed || - !trackedDownload.DownloadItem.CanBeRemoved || + if (!trackedDownload.DownloadItem.CanBeRemoved || trackedDownload.DownloadItem.Status == DownloadItemStatus.Downloading) { return; @@ -74,8 +75,7 @@ public void Handle(DownloadCanBeRemovedEvent message) var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); var definition = downloadClient.Definition as DownloadClientDefinition; - if (trackedDownload.DownloadItem.Removed || - !trackedDownload.DownloadItem.CanBeRemoved || + if (!trackedDownload.DownloadItem.CanBeRemoved || !definition.RemoveCompletedDownloads) { return; @@ -90,7 +90,7 @@ private void RemoveFromDownloadClient(TrackedDownload trackedDownload, IDownload { _logger.Debug("[{0}] Removing download from {1} history", trackedDownload.DownloadItem.Title, trackedDownload.DownloadItem.DownloadClientInfo.Name); downloadClient.RemoveItem(trackedDownload.DownloadItem, true); - trackedDownload.DownloadItem.Removed = true; + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); } catch (NotSupportedException) { diff --git a/src/NzbDrone.Core/Download/DownloadProcessingService.cs b/src/NzbDrone.Core/Download/DownloadProcessingService.cs index cbc57c48b..0251c2dd7 100644 --- a/src/NzbDrone.Core/Download/DownloadProcessingService.cs +++ b/src/NzbDrone.Core/Download/DownloadProcessingService.cs @@ -35,7 +35,7 @@ public DownloadProcessingService(IConfigService configService, private void RemoveCompletedDownloads() { var trackedDownloads = _trackedDownloadService.GetTrackedDownloads() - .Where(t => !t.DownloadItem.Removed && t.DownloadItem.CanBeRemoved && t.State == TrackedDownloadState.Imported) + .Where(t => t.DownloadItem.CanBeRemoved && t.State == TrackedDownloadState.Imported) .ToList(); foreach (var trackedDownload in trackedDownloads) diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index cc0dcfc4a..576c374e8 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -22,7 +22,9 @@ public static string WithDefault(this string actual, object defaultValue) public static long Round(this long number, long level) { - return Convert.ToInt64(Math.Floor((decimal)number / level) * level); + return number < 0 + ? Convert.ToInt64(Math.Ceiling((decimal)number / level) * level) + : Convert.ToInt64(Math.Floor((decimal)number / level) * level); } public static string ToBestDateString(this DateTime dateTime) diff --git a/src/NzbDrone.Core/Housekeeping/HousekeepingCommand.cs b/src/NzbDrone.Core/Housekeeping/HousekeepingCommand.cs index ebf482ee5..937bf3794 100644 --- a/src/NzbDrone.Core/Housekeeping/HousekeepingCommand.cs +++ b/src/NzbDrone.Core/Housekeeping/HousekeepingCommand.cs @@ -4,5 +4,6 @@ namespace NzbDrone.Core.Housekeeping { public class HousekeepingCommand : Command { + public override bool IsExclusive => true; } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs index b1ad00098..13115fb97 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.ImportLists.Trakt { public class TraktSeriesIdsResource { - public int Trakt { get; set; } + public int? Trakt { get; set; } public string Slug { get; set; } public string Imdb { get; set; } public int? Tmdb { get; set; } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs index b7397138c..fc2e2f1ad 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs @@ -109,7 +109,7 @@ private string GetUserName(string accessToken) } catch (HttpException) { - _logger.Warn($"Error refreshing trakt access token"); + _logger.Warn("Error retrieving Trakt user settings"); } return null; @@ -125,26 +125,22 @@ private void RefreshToken() .AddQueryParam("refresh_token", Settings.RefreshToken) .Build(); - try + var response = _httpClient.Get(request); + + if (response?.Resource == null) { - var response = _httpClient.Get(request); - - if (response != null && response.Resource != null) - { - var token = response.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); - } - } + _logger.Warn("Trakt token refresh returned an empty response"); + return; } - catch (HttpException) + + var token = response.Resource; + Settings.AccessToken = token.AccessToken; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); + Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken; + + if (Definition.Id > 0) { - _logger.Warn($"Error refreshing trakt access token"); + _importListRepository.UpdateSettings((ImportListDefinition)Definition); } } } diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index 5c5c1a3fb..f7e46bdd9 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -37,7 +37,7 @@ public static string GetCleanSceneTitle(string title) // remove any repeating +s cleanTitle = Regex.Replace(cleanTitle, @"\+{2,}", "+"); - cleanTitle = cleanTitle.RemoveAccent(); + cleanTitle = cleanTitle.RemoveDiacritics(); return cleanTitle.Trim('+', ' '); } } diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index c4931b53c..0a7da938c 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -151,7 +151,7 @@ public void Execute(MissingEpisodeSearchCommand message) pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } - episodes = _episodeService.EpisodesWithoutFiles(pagingSpec).Records.ToList(); + episodes = _episodeService.EpisodesWithoutFiles(pagingSpec, true).Records.ToList(); } var queue = GetQueuedEpisodeIds(); diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 6625015c7..3af963f78 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -102,6 +102,12 @@ public async Task> SeasonSearch(int seriesId, int seasonN episodes = episodes.Where(e => !e.HasFile).ToList(); } + if (episodes.Count == 0) + { + _logger.Debug("No wanted episodes found for season {0}", seasonNumber); + return new List(); + } + return await SeasonSearch(seriesId, seasonNumber, episodes, monitoredOnly, userInvokedSearch, interactiveSearch); } diff --git a/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs index 4f4f6b9ad..24dfdee26 100644 --- a/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/SeriesSearchService.cs @@ -35,6 +35,7 @@ public void Execute(SeriesSearchCommand message) var series = _seriesService.GetSeries(message.SeriesId); var downloadedCount = 0; var userInvokedSearch = message.Trigger == CommandTrigger.Manual; + var profile = series.QualityProfile.Value; if (series.Seasons.None(s => s.Monitored)) { @@ -64,7 +65,7 @@ public void Execute(SeriesSearchCommand message) continue; } - var decisions = _releaseSearchService.SeasonSearch(message.SeriesId, season.SeasonNumber, false, true, userInvokedSearch, false).GetAwaiter().GetResult(); + var decisions = _releaseSearchService.SeasonSearch(message.SeriesId, season.SeasonNumber, !profile.UpgradeAllowed, true, userInvokedSearch, false).GetAwaiter().GetResult(); var processDecisions = _processDownloadDecisions.ProcessDecisions(decisions).GetAwaiter().GetResult(); downloadedCount += processDecisions.Grabbed.Count; } diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 6fca9e540..b22e1b669 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -96,9 +96,9 @@ public IndexerDefinition ResolveIndexer(int? id, string name) { var all = All(); var clientByName = name.IsNullOrWhiteSpace() ? null : all.FirstOrDefault(c => c.Name.EqualsIgnoreCase(name)); - var clientById = id.HasValue ? all.FirstOrDefault(c => c.Id == id.Value) : null; + var clientById = id is > 0 ? all.FirstOrDefault(c => c.Id == id.Value) : null; - if (id.HasValue && clientById == null) + if (id is > 0 && clientById == null) { throw new ResolveIndexerException("Indexer with ID '{0}' could not be found", id.Value); } @@ -115,7 +115,7 @@ public IndexerDefinition ResolveIndexer(int? id, string name) if (clientByName != null && clientById != null && clientByName.Id != clientById.Id) { - throw new ResolveIndexerException("Indexer with name '{0}' does not match Indexerwith ID '{1}'", name, id.Value); + throw new ResolveIndexerException("Indexer with name '{0}' does not match indexer with ID '{1}'", name, id.Value); } return clientById ?? clientByName; diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index 4f4f5ce43..012ed823a 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -186,7 +186,9 @@ protected virtual string GetGuid(XElement item) protected virtual string GetTitle(XElement item) { - return item.TryGetValue("title", "Unknown"); + var title = item.TryGetValue("title", "Unknown"); + + return WebUtility.HtmlDecode(title); } protected virtual DateTime GetPublishDate(XElement item) diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index 9131d047e..057a85371 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -1,4 +1,5 @@ { + "About": "نبدة عن", "AddAutoTag": "أضف كلمات دلالية تلقائيا", "AddCondition": "إضافة شرط", "AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.", diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 380454a2a..85c0a19f9 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -13,6 +13,7 @@ "AddConditionError": "No es pot afegir una condició nova, torneu-ho a provar.", "AddConditionImplementation": "Afegeix una condició - {implementationName}", "AddConnection": "Afegeix una connexió", + "AddConnectionError": "Incapaç d'afegir una nova connexió, si us plau torneu-ho a intentar.", "AddConnectionImplementation": "Afegeix una connexió - {implementationName}", "AddCustomFilter": "Afegeix un filtre personalitzat", "AddCustomFormat": "Afegeix un format personalitzat", @@ -166,6 +167,8 @@ "BlocklistRelease": "Publicació de la llista de bloqueig", "BlocklistReleaseHelpText": "Impedeix que {appName} baixi aquesta versió mitjançant RSS o cerca automàtica", "BlocklistReleases": "Llista de llançaments bloquejats", + "Blocklisted": "Bloquejat per llista", + "BlocklistedAt": "Bloquejat per llista el {date}", "Branch": "Branca", "BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}", "BranchUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern", @@ -253,6 +256,7 @@ "ConnectionLostToBackend": "{appName} ha perdut la connexió amb el backend i s'haurà de tornar a carregar per a restaurar la funcionalitat.", "ConnectionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}", "Connections": "Connexions", + "ConnectionsLoadError": "Incapaç de carregar Connexions", "Continuing": "Continua", "ContinuingOnly": "Només en emissió", "ContinuingSeriesDescription": "S'esperen més episodis o altra temporada", @@ -555,12 +559,13 @@ "DownloadClientTriblerSettingsDirectoryHelpText": "Ubicació opcional per a desar les baixades, deixeu-ho en blanc per utilitzar la ubicació predeterminada del Tribler", "DownloadClientTriblerSettingsSafeSeeding": "Compartició segura", "DownloadClientTriblerSettingsSafeSeedingHelpText": "Quan està activat, només es comparteix a través de proxies.", + "DownloadClientUTorrentProviderMessage": "uTorrent té historial d'incloure criptominers, malware i anuncis, suggerim fortament que escolleixis un client diferent.", "DownloadClientUTorrentTorrentStateError": "uTorrent està informant d'un error", "DownloadClientUnavailable": "Client de baixada no disponible", "DownloadClientValidationApiKeyIncorrect": "Clau API incorrecta", "DownloadClientValidationApiKeyRequired": "Clau API requerida", "DownloadClientValidationAuthenticationFailure": "Error d'autenticació", - "DownloadClientValidationAuthenticationFailureDetail": "Verifiqueu el vostre nom d'usuari i contrasenya. Verifiqueu també si el servidor que executa {appName} no està bloquejat per accedir a {clientName} per les limitacions de WhiteList a la configuració {clientName}.", + "DownloadClientValidationAuthenticationFailureDetail": "Verifiqueu el vostre nom d'usuari i contrasenya. Verifiqueu també si el servidor que executa {appName} no està bloquejat d'accedir a {clientName} per les limitacions de WhiteList a la configuració de {clientName}.", "DownloadClientValidationCategoryMissing": "La categoria no existeix", "DownloadClientValidationCategoryMissingDetail": "La categoria que heu introduït no existeix a {clientName}. Primer creeu-lo a {clientName}.", "DownloadClientValidationErrorVersion": "La versió de {clientName} hauria de ser com a mínim {requiredVersion}. La versió informada és {reportedVersion}", @@ -781,6 +786,7 @@ "Formats": "Formats", "Forums": "Fòrums", "FreeSpace": "Espai lliure", + "Friday": "Divendres", "From": "Des de", "FullColorEvents": "Esdeveniments a tot color", "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", @@ -803,6 +809,8 @@ "HasMissingSeason": "Té temporades que falten", "HasUnmonitoredSeason": "Té una temporada no monitorada", "Health": "Salut", + "HealthIssue": "1 problema de salut", + "HealthIssues": "{count} problemes de salut", "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.", "Here": "aquí", "HiddenClickToShow": "Amagat, feu clic per a mostrar", @@ -839,6 +847,10 @@ "IgnoreDownloadsHint": "Atura {appName} de processar aquestes baixades més", "Ignored": "Ignorat", "IgnoredAddresses": "Adreces ignorades", + "ImageBanner": "bàner", + "ImageFanart": "art de fans", + "ImagePoster": "pòster", + "ImageSeason": "temporada", "Images": "Imatges", "ImdbId": "ID d'IMDb", "Implementation": "Implementació", @@ -1191,8 +1203,12 @@ "MaximumSizeHelpText": "Mida màxima per a una versió que es pot capturar en MB. Establiu a zero per a establir-lo en il·limitat", "Mechanism": "Mecanisme", "MediaInfo": "Informació multimèdia", + "MediaInfoAudioStreamHeader": "Transmissió d'àudio #{number}", "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages suporta un sufix `EN+DE` que permet filtrar les llengües incloses en el nom de fitxer. Utilitzeu `-DE` per excloure llengües específiques. Afegint `+` (ex. `:EN+`) sortirà `[EN]`/`[EN+--]`/`[--]` depenent de les llengües excloses. Per exemple `{MediaInfo Full:EN+DE}`.", "MediaInfoFootNote2": "MediaInfo AudioLanguages exclou l'anglès si és l'únic idioma. Usa MediaInfo AudioLanguagesAll per incloure només l'anglès", + "MediaInfoForced": "Forçat", + "MediaInfoHearingImpaired": "Dificultats auditives", + "MediaInfoSubtitlesHeader": "Subtítols", "MediaManagement": "Gestió multimèdia", "MediaManagementSettings": "Configuració de la gestió multimèdia", "MediaManagementSettingsLoadError": "No s'han pogut carregar la configuració de gestió multimèdia", @@ -1684,6 +1700,8 @@ "Queue": "Cua", "QueueFilterHasNoItems": "El filtre de cua seleccionat no té elements", "QueueIsEmpty": "La cua és buida", + "QueueItem": "1 element a la cua", + "QueueItems": "{count} elements a la cua", "QueueLoadError": "No s'ha pogut carregar la cua", "Queued": "En cua", "QuickSearch": "Cerca ràpida", @@ -1845,6 +1863,7 @@ "RssSyncIntervalHelpText": "Interval en minuts. Establiu a zero per a desactivar (això aturarà tota la captura automàtica de versions)", "RssSyncIntervalHelpTextWarning": "Això s'aplicarà a tots els indexadors, seguiu les regles establertes per ells", "Runtime": "Temps d'execució", + "Saturday": "Dissabte", "Save": "Desa", "SaveChanges": "Desa els canvis", "SaveSettings": "Desa la configuració", @@ -1893,6 +1912,8 @@ "SeasonPremiere": "Estrena de temporada", "SeasonPremieresOnly": "Només estrenes de temporada", "Seasons": "Temporades", + "SeasonsMonitoredAll": "Tot", + "SeasonsMonitoredNone": "Cap", "SeasonsMonitoredStatus": "Temporades monitorades", "SecretToken": "Testimoni secret", "Security": "Seguretat", @@ -2040,6 +2061,7 @@ "SupportedListsMoreInfo": "Per a més informació sobre les llistes individuals, feu clic als botons de més informació.", "SupportedListsSeries": "{appName} admet diverses llistes per importar sèries a la base de dades.", "System": "Sistema", + "SystemDefault": "Per defecte del sistema", "SystemTimeHealthCheckMessage": "L'hora del sistema està desfasada més d'1 dia. Les tasques planificades no es poden executar correctament fins que no es corregeixi el temps", "Table": "Taula", "TableColumns": "Columnes", @@ -2069,9 +2091,12 @@ "TheTvdb": "TheTVDB", "Theme": "Tema", "ThemeHelpText": "Canvia el tema de la interfície d'usuari de l'aplicació, el tema 'Automàtic' utilitzarà el tema del sistema per establir el mode clar o fosc. Inspirat per Theme.Park", + "Threshold": "Llindar", + "Thursday": "Dijous", "Time": "Temps", "TimeFormat": "Format de l'hora", "TimeLeft": "Temps a l'esquerra", + "TimeZone": "Zona horària", "Title": "Títol", "Titles": "Títols", "Today": "Avui", @@ -2100,6 +2125,7 @@ "TotalSpace": "Espai total", "Trace": "Rastreig", "True": "Vertader", + "Tuesday": "Dimarts", "TvdbId": "ID de TVDB", "TvdbIdExcludeHelpText": "L'ID de TVDB de la sèrie a excloure", "Twitter": "Twitter", @@ -2199,6 +2225,7 @@ "Wanted": "Demanat", "Warn": "Avís", "Warning": "Avís", + "Wednesday": "Dimecres", "Week": "Setmana", "WeekColumnHeader": "Capçalera de la columna de la setmana", "WeekColumnHeaderHelpText": "Es mostra per sobre de cada columna quan la setmana és la vista activa", diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 681daa3c0..eecc14ee2 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -13,6 +13,7 @@ "AddConditionError": "Neue Bedingung konnte nicht hinzugefügt werden, bitte erneut versuchen.", "AddConditionImplementation": "Bedingung hinzufügen - {implementationName}", "AddConnection": "Verbindung hinzufügen", + "AddConnectionError": "Neue Verbindung konnte nicht hinzugefügt werden. Bitte erneut versuchen.", "AddConnectionImplementation": "Verbindung hinzufügen - {implementationName}", "AddCustomFilter": "Eigenen Filter hinzufügen", "AddCustomFormat": "Eigenes Format hinzufügen", diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 75521aede..d4a50a186 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -57,11 +57,16 @@ "AddedDate": "Added: {date}", "AddedToDownloadQueue": "Added to download queue", "AddingTag": "Adding tag", + "AdvancedSettings": "Advanced Settings", "AfterManualRefresh": "After Manual Refresh", "Age": "Age", "AgeWhenGrabbed": "Age (when grabbed)", "Agenda": "Agenda", "AirDate": "Air Date", + "AirDateGracePeriod": "Air Date Grace Period", + "AirDateGracePeriodHelpText": "Negative values allow grabbing before the air date, positive values prevent grabbing after the air date.", + "AirDateRestriction": "Reject Unaired Releases", + "AirDateRestrictionHelpText": "Prevents {appName} from grabbing releases that contain episodes that have not yet aired.", "Airs": "Airs", "AirsDateAtTimeOn": "{date} at {time} on {networkLabel}", "AirsTbaOn": "TBA on {networkLabel}", @@ -132,6 +137,7 @@ "AutoTaggingSpecificationMaximumYear": "Maximum Year", "AutoTaggingSpecificationMinimumYear": "Minimum Year", "AutoTaggingSpecificationNetwork": "Network(s)", + "AutoTaggingSpecificationOriginalCountry": "Country", "AutoTaggingSpecificationOriginalLanguage": "Language", "AutoTaggingSpecificationQualityProfile": "Quality Profile", "AutoTaggingSpecificationRootFolder": "Root Folder", @@ -142,6 +148,8 @@ "AutomaticAdd": "Automatic Add", "AutomaticSearch": "Automatic Search", "AutomaticUpdatesDisabledDocker": "Automatic updates are not directly supported when using the Docker update mechanism. You will need to update the container image outside of {appName} or use a script", + "AverageSize": "Average Size", + "AverageSizePerEpisode": "Average Size per Episode", "Backup": "Backup", "BackupFolderHelpText": "Relative paths will be under {appName}'s AppData directory", "BackupIntervalHelpText": "Interval between automatic backups", @@ -359,6 +367,7 @@ "DeleteEpisodeFromDisk": "Delete episode from disk", "DeleteEpisodesFiles": "Delete {episodeFileCount} Episode Files", "DeleteEpisodesFilesHelpText": "Delete the episode files and series folder", + "DeleteFiles": "Delete Files", "DeleteImportList": "Delete Import List", "DeleteImportListExclusion": "Delete Import List Exclusion", "DeleteImportListExclusionMessageText": "Are you sure you want to delete this import list exclusion?", @@ -386,6 +395,8 @@ "DeleteSelectedIndexers": "Delete Indexer(s)", "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {count} selected indexer(s)?", "DeleteSelectedSeries": "Delete Selected Series", + "DeleteSelectedSeriesFiles": "Delete Selected Series Files", + "DeleteSeriesFilesConfirmation": "Are you sure you want to delete all tracked episode files for {count} selected series?", "DeleteSeriesFolder": "Delete Series Folder", "DeleteSeriesFolderConfirmation": "The series folder `{path}` and all of its content will be deleted.", "DeleteSeriesFolderCountConfirmation": "Are you sure you want to delete {count} selected series?", @@ -657,6 +668,7 @@ "EpisodeFileDeleted": "Episode File Deleted", "EpisodeFileDeletedTooltip": "Episode file deleted", "EpisodeFileMissingTooltip": "Episode file missing", + "EpisodeFileQualities": "Episode File Qualities", "EpisodeFileRenamed": "Episode File Renamed", "EpisodeFileRenamedTooltip": "Episode file renamed", "EpisodeFilesLoadError": "Unable to load episode files", @@ -697,6 +709,7 @@ "Events": "Events", "Example": "Example", "Exception": "Exception", + "ExcludeSpecials": "Exclude Specials", "ExcludeUnknownSeriesItems": "Exclude Unknown Series Items", "ExcludedReleaseProfile": "Excluded Release Profile", "ExcludedReleaseProfiles": "Excluded Release Profiles", @@ -786,6 +799,7 @@ "Formats": "Formats", "Forums": "Forums", "FreeSpace": "Free Space", + "Friday": "Friday", "From": "From", "FullColorEvents": "Full Color Events", "FullColorEventsHelpText": "Altered style to color the entire event with the status color, instead of just the left edge. Does not apply to Agenda", @@ -943,7 +957,7 @@ "ImportListsTraktSettingsAdditionalParametersHelpText": "Additional Trakt API parameters", "ImportListsTraktSettingsAuthenticateWithTrakt": "Authenticate with Trakt", "ImportListsTraktSettingsGenres": "Genres", - "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre slug (action,comedy)", + "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre slug, comma separated (action,comedy)", "ImportListsTraktSettingsLimit": "Limit", "ImportListsTraktSettingsLimitSeriesHelpText": "Limit the number of series to get", "ImportListsTraktSettingsListName": "List Name", @@ -1099,6 +1113,7 @@ "InstanceName": "Instance Name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InteractiveImport": "Interactive Import", + "InteractiveImportDuplicateEpisodes": "One or more episodes were assigned to multiple files", "InteractiveImportLoadError": "Unable to load manual import items", "InteractiveImportMultipleQueueItems": "Multiple Queue Items", "InteractiveImportNoEpisode": "One or more episodes must be chosen for each selected file", @@ -1173,6 +1188,7 @@ "Logs": "Logs", "LongDateFormat": "Long Date Format", "Lowercase": "Lowercase", + "MainNavigation": "Main Navigation", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "ManageClients": "Manage Clients", "ManageCustomFormats": "Manage Custom Formats", @@ -1612,6 +1628,7 @@ "OrganizeSelectedSeriesModalConfirmation": "Are you sure you want to organize all files in the {count} selected series?", "OrganizeSelectedSeriesModalHeader": "Organize Selected Series", "Original": "Original", + "OriginalCountry": "Original Country", "OriginalLanguage": "Original Language", "Other": "Other", "OutputPath": "Output Path", @@ -1625,6 +1642,11 @@ "OverviewOptions": "Overview Options", "PackageVersion": "Package Version", "PackageVersionInfo": "{packageVersion} by {packageAuthor}", + "PagerGoToFirstPage": "Go to first page", + "PagerGoToLastPage": "Go to last page", + "PagerGoToNextPage": "Go to next page", + "PagerGoToPage": "Go to page {page} of {totalPages}", + "PagerGoToPreviousPage": "Go to previous page", "Parse": "Parse", "ParseModalErrorParsing": "Error parsing, please try again.", "ParseModalHelpText": "Enter a release title in the input above", @@ -1693,6 +1715,9 @@ "QualityDefinitionsSizeNotice": "Size restrictions have been moved to Quality Profiles", "QualityProfile": "Quality Profile", "QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection", + "QualityProfileUsage": "Quality Profile Usage", + "QualityProfileUsedInCountImportLists": "Used in {count} import lists", + "QualityProfileUsedInCountSeries": "Used in {count} series", "QualityProfiles": "Quality Profiles", "QualityProfilesLoadError": "Unable to load Quality Profiles", "QualitySettings": "Quality Settings", @@ -1752,6 +1777,7 @@ "ReleaseSource": "Release Source", "ReleaseTitle": "Release Title", "ReleaseType": "Release Type", + "ReleaseTypes": "Release Types", "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.", @@ -1862,7 +1888,9 @@ "RssSyncInterval": "RSS Sync Interval", "RssSyncIntervalHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)", "RssSyncIntervalHelpTextWarning": "This will apply to all indexers, please follow the rules set forth by them", + "Run": "Run", "Runtime": "Runtime", + "Saturday": "Saturday", "Save": "Save", "SaveChanges": "Save Changes", "SaveSettings": "Save Settings", @@ -1951,6 +1979,7 @@ "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", + "SeriesInImportListExclusions": "Series is in Import List Exclusions", "SeriesIndexFooterContinuing": "Continuing (All episodes downloaded)", "SeriesIndexFooterDownloading": "Downloading (One or more episodes)", "SeriesIndexFooterEnded": "Ended (All episodes downloaded)", @@ -2091,6 +2120,7 @@ "Theme": "Theme", "ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by Theme.Park", "Threshold": "Threshold", + "Thursday": "Thursday", "Time": "Time", "TimeFormat": "Time Format", "TimeLeft": "Time Left", @@ -2123,6 +2153,7 @@ "TotalSpace": "Total Space", "Trace": "Trace", "True": "True", + "Tuesday": "Tuesday", "TvdbId": "TVDB ID", "TvdbIdExcludeHelpText": "The TVDB ID of the series to exclude", "Twitter": "Twitter", @@ -2215,6 +2246,7 @@ "VideoCodec": "Video Codec", "VideoDynamicRange": "Video Dynamic Range", "View": "View", + "ViewSeriesOnTvdb": "View {title} on TVDB", "VisitTheWikiForMoreDetails": "Visit the wiki for more details: ", "WaitingToImport": "Waiting to Import", "WaitingToProcess": "Waiting to Process", @@ -2222,6 +2254,7 @@ "Wanted": "Wanted", "Warn": "Warn", "Warning": "Warning", + "Wednesday": "Wednesday", "Week": "Week", "WeekColumnHeader": "Week Column Header", "WeekColumnHeaderHelpText": "Shown above each column when week is the active view", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index 7ec12946f..c975a57ec 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -13,6 +13,7 @@ "AddConditionError": "No se ha podido añadir una nueva condición, prueba otra vez.", "AddConditionImplementation": "Añadir condición - {implementationName}", "AddConnection": "Añadir Conexión", + "AddConnectionError": "No se puede añadir una nueva conexión, por favor inténtalo de nuevo.", "AddConnectionImplementation": "Añadir Conexión - {implementationName}", "AddCustomFilter": "Añadir Filtro Personalizado", "AddCustomFormat": "Añadir Formato Personalizado", @@ -56,11 +57,16 @@ "AddedDate": "Agregado: {date}", "AddedToDownloadQueue": "Añadido a la cola de descarga", "AddingTag": "Añadir etiqueta", + "AdvancedSettings": "Configuración avanzada", "AfterManualRefresh": "Tras Refrescar Manualmente", "Age": "Antigüedad", "AgeWhenGrabbed": "Antigüedad (cuando se añadió)", "Agenda": "Agenda", "AirDate": "Fecha de Emisión", + "AirDateGracePeriod": "Período de gracia de la fecha de emisión", + "AirDateGracePeriodHelpText": "Valores negativos permiten capturar antes de la fecha de emisión, valores positivos evitan capturar después de la fecha de emisión.", + "AirDateRestriction": "Rechazar lanzamientos sin emitir", + "AirDateRestrictionHelpText": "Evita que {appName} capture lanzamientos que contienen episodios que aún no se han emitido.", "Airs": "Emision", "AirsDateAtTimeOn": "{date} en {time} en{networkLabel}", "AirsTbaOn": "A anunciar en {networkLabel}", @@ -131,6 +137,7 @@ "AutoTaggingSpecificationMaximumYear": "Año máximo", "AutoTaggingSpecificationMinimumYear": "Año mínimo", "AutoTaggingSpecificationNetwork": "Red(es)", + "AutoTaggingSpecificationOriginalCountry": "País", "AutoTaggingSpecificationOriginalLanguage": "Idioma", "AutoTaggingSpecificationQualityProfile": "Perfil de calidad", "AutoTaggingSpecificationRootFolder": "Carpeta raíz", @@ -141,6 +148,8 @@ "AutomaticAdd": "Añadir Automáticamente", "AutomaticSearch": "Búsqueda Automática", "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", + "AverageSize": "Tamaño promedio", + "AverageSizePerEpisode": "Tamaño promedio por episodio", "Backup": "Copia de seguridad", "BackupFolderHelpText": "Las rutas relativas estarán en el directorio AppData de {appName}", "BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas", @@ -166,6 +175,8 @@ "BlocklistRelease": "Lista de bloqueos de lanzamiento", "BlocklistReleaseHelpText": "Bloquea este lanzamiento de volver a ser descargado por {appName} vía RSS o búsqueda automática", "BlocklistReleases": "Lista de bloqueos de lanzamientos", + "Blocklisted": "Bloqueado", + "BlocklistedAt": "Bloqueado el {date}", "Branch": "Rama", "BranchUpdate": "Rama a usar para actualizar {appName}", "BranchUpdateMechanism": "Rama usada por un mecanismo de actualización externo", @@ -356,6 +367,7 @@ "DeleteEpisodeFromDisk": "Eliminar episodio del disco", "DeleteEpisodesFiles": "Eliminar {episodeFileCount} archivos de episodios", "DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series", + "DeleteFiles": "Eliminar archivos", "DeleteImportList": "Eliminar lista de importación", "DeleteImportListExclusion": "Eliminar exclusión de listas de importación", "DeleteImportListExclusionMessageText": "¿Estás seguro que quieres eliminar esta exclusión de la lista de importación?", @@ -383,6 +395,8 @@ "DeleteSelectedIndexers": "Borrar indexador(es)", "DeleteSelectedIndexersMessageText": "¿Estás seguro que quieres eliminar {count} indexador(es) seleccionado(s)?", "DeleteSelectedSeries": "Eliminar serie seleccionada", + "DeleteSelectedSeriesFiles": "Eliminar archivos de series seleccionadas", + "DeleteSeriesFilesConfirmation": "¿Estás seguro de que quieres eliminar todos los archivos de episodios rastreados para las {count} series seleccionadas?", "DeleteSeriesFolder": "Eliminar directorio de series", "DeleteSeriesFolderConfirmation": "El directorio de series '{path}' y todos sus contenidos seran eliminados.", "DeleteSeriesFolderCountConfirmation": "Esta seguro que desea eliminar '{count}' series seleccionadas?", @@ -556,6 +570,7 @@ "DownloadClientTriblerSettingsDirectoryHelpText": "Localización opcional en la que poner las descargas, dejar en blanco para usar la localización predeterminada de Tribler", "DownloadClientTriblerSettingsSafeSeeding": "Sembrado seguro", "DownloadClientTriblerSettingsSafeSeedingHelpText": "Cuando se habilita, solo se siembra a través de los proxies.", + "DownloadClientUTorrentProviderMessage": "uTorrent tiene un amplio historial de incluir criptomineros, malware y publicidad, por lo que recomendamos encarecidamente que elijas un cliente diferente.", "DownloadClientUTorrentTorrentStateError": "uTorrent está reportando un error", "DownloadClientUnavailable": "Cliente de descarga no disponible", "DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta", @@ -693,6 +708,7 @@ "Events": "Eventos", "Example": "Ejemplo", "Exception": "Excepción", + "ExcludeSpecials": "Excluir especiales", "ExcludeUnknownSeriesItems": "Excluir elementos de series desconocidas", "ExcludedReleaseProfile": "Perfil de lanzamiento excluido", "ExcludedReleaseProfiles": "Perfiles de lanzamiento excluidos", @@ -782,6 +798,7 @@ "Formats": "Formatos", "Forums": "Foros", "FreeSpace": "Espacio libre", + "Friday": "Viernes", "From": "desde", "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", @@ -1094,6 +1111,7 @@ "InstanceName": "Nombre de la Instancia", "InstanceNameHelpText": "Nombre de la instancia en la pestaña y para la aplicación Syslog", "InteractiveImport": "Importación Interactiva", + "InteractiveImportDuplicateEpisodes": "Uno o más episodios fueron asignados a múltiples archivos", "InteractiveImportLoadError": "No se pueden cargar elementos de la importación manual", "InteractiveImportMultipleQueueItems": "Múltiples colas de elementos", "InteractiveImportNoEpisode": "Hay que elegir uno o varios episodios para cada archivo seleccionado", @@ -1168,6 +1186,7 @@ "Logs": "Registros", "LongDateFormat": "Formato de Fecha Larga", "Lowercase": "Minúscula", + "MainNavigation": "Navegación principal", "MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver el historial de commits de Github para más detalles", "ManageClients": "Administrar Clientes", "ManageCustomFormats": "Gestionar formatos personalizados", @@ -1607,6 +1626,7 @@ "OrganizeSelectedSeriesModalConfirmation": "¿Estás seguro que quieres organizar todos los archivos en las {count} series seleccionadas?", "OrganizeSelectedSeriesModalHeader": "Organizar series seleccionadas", "Original": "Original", + "OriginalCountry": "País original", "OriginalLanguage": "Idioma original", "Other": "Otro", "OutputPath": "Ruta de salida", @@ -1620,6 +1640,11 @@ "OverviewOptions": "Opciones de vista general", "PackageVersion": "Versión del paquete", "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "PagerGoToFirstPage": "Ir a la primera página", + "PagerGoToLastPage": "Ir a la última página", + "PagerGoToNextPage": "Ir a la siguiente página", + "PagerGoToPage": "Ir a la página {page} de {totalPages}", + "PagerGoToPreviousPage": "Ir a la página anterior", "Parse": "Analizar", "ParseModalErrorParsing": "Error analizando, por favor inténtalo de nuevo.", "ParseModalHelpText": "Introduce un título de lanzamiento en la entrada anterior", @@ -1688,6 +1713,9 @@ "QualityDefinitionsSizeNotice": "Las restricciones de tamaño se han movido a los Perfiles de Calidad", "QualityProfile": "Perfil de calidad", "QualityProfileInUseSeriesListCollection": "No se puede borrar un perfil de calidad que está asignado a una serie, lista o colección", + "QualityProfileUsage": "Uso de perfil de calidad", + "QualityProfileUsedInCountImportLists": "Usado en {count} listas de importación", + "QualityProfileUsedInCountSeries": "Usado en {count} series", "QualityProfiles": "Perfiles de calidad", "QualityProfilesLoadError": "No se pudo cargar los perfiles de calidad", "QualitySettings": "Opciones de calidad", @@ -1857,7 +1885,9 @@ "RssSyncInterval": "Intervalo de sincronización RSS", "RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)", "RssSyncIntervalHelpTextWarning": "Esto se aplicará a todos los indexadores, por favor sigue las reglas establecidas por ellos", + "Run": "Ejecutar", "Runtime": "Tiempo de duración", + "Saturday": "Sábado", "Save": "Guardar", "SaveChanges": "Guardar cambios", "SaveSettings": "Guardar ajustes", @@ -1906,6 +1936,8 @@ "SeasonPremiere": "Estreno de temporada", "SeasonPremieresOnly": "Solo estrenos de temporada", "Seasons": "Temporadas", + "SeasonsMonitoredAll": "Todo", + "SeasonsMonitoredNone": "Ninguno", "SeasonsMonitoredStatus": "Temporadas monitorizadas", "SecretToken": "Token secreto", "Security": "Seguridad", @@ -1929,7 +1961,7 @@ "SelectSeries": "Seleccionar Series", "SendAnonymousUsageData": "Enviar datos de uso anónimos", "Series": "Series", - "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}) .", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}).", "SeriesCannotBeFound": "Lo siento, esta serie no puede ser encontrada.", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio", "SeriesDetailsGoTo": "Ir a {title}", @@ -1944,6 +1976,7 @@ "SeriesFolderImportedTooltip": "Episodio importado de la carpeta de serie", "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}`).", "SeriesID": "ID de serie", + "SeriesInImportListExclusions": "La serie está en las exclusiones de la lista de importación", "SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)", "SeriesIndexFooterDownloading": "Descargando (Uno o más episodios)", "SeriesIndexFooterEnded": "FInalizado (Todos los episodios descargados)", @@ -2053,6 +2086,7 @@ "SupportedListsMoreInfo": "Para más información en las listas 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.", "System": "Sistema", + "SystemDefault": "Predeterminado del sistema", "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", "Table": "Tabla", "TableColumns": "Columnas", @@ -2083,9 +2117,11 @@ "Theme": "Tema", "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", "Threshold": "Umbral", + "Thursday": "Jueves", "Time": "Tiempo", "TimeFormat": "Formato de hora", "TimeLeft": "Tiempo restante", + "TimeZone": "Zona horaria", "Title": "Título", "Titles": "Títulos", "Today": "Hoy", @@ -2114,6 +2150,7 @@ "TotalSpace": "Espacio Total", "Trace": "Traza", "True": "Verdadero", + "Tuesday": "Martes", "TvdbId": "TVDB ID", "TvdbIdExcludeHelpText": "La ID de TVDB de la serie a excluir", "Twitter": "Twitter", @@ -2206,6 +2243,7 @@ "VideoCodec": "Códec de vídeo", "VideoDynamicRange": "Video de Rango Dinámico", "View": "Vista", + "ViewSeriesOnTvdb": "Ver {title} en TVDB", "VisitTheWikiForMoreDetails": "Visita la wiki para más detalles: ", "WaitingToImport": "Esperar para importar", "WaitingToProcess": "Esperar al proceso", @@ -2213,6 +2251,7 @@ "Wanted": "Buscado", "Warn": "Advertencia", "Warning": "Aviso", + "Wednesday": "Miércoles", "Week": "Semana", "WeekColumnHeader": "Cabecera de columna de semana", "WeekColumnHeaderHelpText": "Mostrado sobre cada columna cuando la vista activa es semana", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 6ae0db020..011a2df78 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -24,7 +24,7 @@ "AddDownloadClientImplementation": "Ajouter un client de téléchargement - {implementationName}", "AddExclusion": "Ajouter une exclusion", "AddImportList": "Ajouter une liste d'importation", - "AddImportListExclusion": "Ajouter une exclusion à la liste des importations", + "AddImportListExclusion": "Ajouter une liste d'exclusion", "AddImportListExclusionError": "Impossible d'ajouter une nouvelle exclusion de liste d'importation, veuillez réessayer.", "AddImportListImplementation": "Ajouter une liste d'importation - {implementationName}", "AddIndexer": "Ajouter un indexeur", @@ -304,7 +304,7 @@ "CustomFormatsSpecificationReleaseGroup": "Groupe de versions", "CustomFormatsSpecificationResolution": "Résolution", "CustomFormatsSpecificationSource": "Source", - "Cutoff": "Seuil", + "Cutoff": "Limite", "CutoffNotMet": "Seuil non atteint", "CutoffUnmet": "Seuil non atteint", "CutoffUnmetLoadError": "Erreur lors du chargement des éléments dont le seuil n'est pas atteint", @@ -843,6 +843,8 @@ "Ignored": "Ignoré", "IgnoredAddresses": "Adresses ignorées", "ImageBanner": "bannière", + "ImageFanart": "fanart", + "ImagePoster": "affiche", "ImageSeason": "saison", "Images": "Images", "ImdbId": "IMDb ID", @@ -991,6 +993,7 @@ "IncludeCustomFormatWhenRenaming": "Inclure un format personnalisé lors du changement de nom", "IncludeCustomFormatWhenRenamingHelpText": "Inclure dans le format de renommage {Formats personnalisés}", "IncludeHealthWarnings": "Inclure les avertissements de santé", + "IncludeSpecials": "Inclure les offres spéciales", "IncludeUnmonitored": "Inclure les non surveillés", "Indexer": "Indexeur", "IndexerDownloadClientHealthCheckMessage": "Indexeurs avec des clients de téléchargement invalides : {indexerNames}.", @@ -1092,6 +1095,7 @@ "InstanceNameHelpText": "Nom de l'instance dans l'onglet et pour le nom de l'application Syslog", "InteractiveImport": "Importation interactive", "InteractiveImportLoadError": "Impossible de charger les éléments d'importation manuelle", + "InteractiveImportMultipleQueueItems": "Éléments de file d'attente multiples", "InteractiveImportNoEpisode": "Un ou plusieurs épisodes doivent être choisis pour chaque fichier sélectionné", "InteractiveImportNoFilesFound": "Aucun fichier vidéo n'a été trouvé dans le dossier sélectionné", "InteractiveImportNoImportMode": "Un mode d'importation doit être sélectionné", @@ -1194,9 +1198,11 @@ "MaximumSizeHelpText": "Taille maximale en Mo pour qu'une version soit récupérée. Réglez sur zéro pour une taille illimitée", "Mechanism": "Mécanisme", "MediaInfo": "Informations médias", + "MediaInfoAudioStreamHeader": "Flux audio #{number}", "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}`.", "MediaInfoFootNote2": "MediaInfo AudioLanguages exclue l’anglais s’il s’agit de la seule langue. Utiliser MediaInfo AudioLanguagesAll pour inclure ceux seulement en anglais", "MediaInfoForced": "Forcé", + "MediaInfoHearingImpaired": "Malentendant", "MediaInfoSubtitlesHeader": "Sous-titres", "MediaManagement": "Gestion des médias", "MediaManagementSettings": "Paramètres de gestion des médias", @@ -2076,6 +2082,7 @@ "TheTvdb": "TheTVDB", "Theme": "Thème", "ThemeHelpText": "Modifiez le thème de l'interface utilisateur de l'application, le thème « Auto » utilisera le thème de votre système d'exploitation pour définir le mode clair ou sombre. Inspiré par Theme.Park", + "Threshold": "Seuil", "Time": "Heure", "TimeFormat": "Format de l'heure", "TimeLeft": "Temps restant", @@ -2134,6 +2141,7 @@ "Unknown": "Inconnu", "UnknownDownloadState": "État de téléchargement inconnu : {state}", "UnknownEventTooltip": "Événement inconnu", + "UnknownSeriesItems": "Éléments de la série inconnus", "Unlimited": "Illimité", "UnmappedFilesOnly": "Fichiers non mappés uniquement", "UnmappedFolders": "Dossiers non mappés", diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index fbb4f000e..4f2ce7656 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -13,6 +13,7 @@ "AddConditionError": "Nem sikerült új feltételt hozzáadni, próbálkozzon újra.", "AddConditionImplementation": "Feltétel hozzáadása – {implementationName}", "AddConnection": "Kapcsolat hozzáadása", + "AddConnectionError": "Nem lehetséges új kapcsolatot hozzáadni, kérem próbálja meg ismét.", "AddConnectionImplementation": "Kapcsolat hozzáadása - {implementationName}", "AddCustomFilter": "Egyéni szűrő hozzáadása", "AddCustomFormat": "Egyéni formátum hozzáadása", @@ -56,11 +57,16 @@ "AddedDate": "Hozzáadva: {date}", "AddedToDownloadQueue": "Hozzáadva a letöltési sorhoz", "AddingTag": "Címke hozzáadása", + "AdvancedSettings": "Speciális beállítások", "AfterManualRefresh": "Kézi frissítés után", "Age": "Kor", "AgeWhenGrabbed": "Kor (amikor megragadták)", "Agenda": "Teendők", "AirDate": "Adásba kerülés dátuma", + "AirDateGracePeriod": "Sugárzási türelmi idő", + "AirDateGracePeriodHelpText": "A negatív értékek lehetővé teszik a letöltést a sugárzási dátum előtt, a pozitív értékek pedig megakadályozzák a letöltést a sugárzási dátum után.", + "AirDateRestriction": "Adásba nem került epizódok elutasítása", + "AirDateRestrictionHelpText": "Megakadályozza, hogy a {appName} olyan kiadásokat töltsön le, amelyek adásba még nem került epizódokat tartalmaznak.", "Airs": "Adásban", "AirsDateAtTimeOn": "{date} {time}-kor a(z) {networkLabel}-on/en/ön", "AirsTbaOn": "Bejelentés alatt a(z) {networkLabel}-on/en/ön", @@ -131,6 +137,7 @@ "AutoTaggingSpecificationMaximumYear": "Maximum Év", "AutoTaggingSpecificationMinimumYear": "Minimum Év", "AutoTaggingSpecificationNetwork": "Hálózat", + "AutoTaggingSpecificationOriginalCountry": "Ország", "AutoTaggingSpecificationOriginalLanguage": "Nyelv", "AutoTaggingSpecificationQualityProfile": "Minőségi Profil", "AutoTaggingSpecificationRootFolder": "Gyökérmappa", @@ -141,6 +148,8 @@ "AutomaticAdd": "Automatikus hozzáadás", "AutomaticSearch": "Automatikus keresés", "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", + "AverageSize": "Átlagos méret", + "AverageSizePerEpisode": "Átlagos méret epizódonként", "Backup": "Biztonsági mentés", "BackupFolderHelpText": "A relatív elérési utak a {appName} AppData könyvtárában találhatók", "BackupIntervalHelpText": "Az automatikus biztonsági mentések közötti időköz", @@ -166,6 +175,8 @@ "BlocklistRelease": "Tiltólista release", "BlocklistReleaseHelpText": "Letiltja ennek a release-nek a letöltését a {appName} által RSS-en vagy automatikus keresésen keresztül", "BlocklistReleases": "Tiltólista release-k", + "Blocklisted": "Tiltólistára téve", + "BlocklistedAt": "{date}-n tiltólistára téve", "Branch": "Kiadási csatorna", "BranchUpdate": "A {appName} frissítéséhez használt kiadási csatorna (ág)", "BranchUpdateMechanism": "Külső frissítési mechanizmus által használt kiadási csatorna (ág)", @@ -182,9 +193,9 @@ "CalendarFeed": "{appName} Naptár Feed", "CalendarLegendEpisodeDownloadedTooltip": "Az epizódot letöltötték és rendezték", "CalendarLegendEpisodeDownloadingTooltip": "Epizód letöltés alatt", - "CalendarLegendEpisodeMissingTooltip": "Az epizódot leadták, és hiányzik a lemezről", + "CalendarLegendEpisodeMissingTooltip": "Az epizód adásba került, és hiányzik a lemezről", "CalendarLegendEpisodeOnAirTooltip": "Az epizód jelenleg adásban van", - "CalendarLegendEpisodeUnairedTooltip": "Az epizódot még nem adták le", + "CalendarLegendEpisodeUnairedTooltip": "Az epizód még nem került adásba", "CalendarLegendEpisodeUnmonitoredTooltip": "Az epizód nem figyelhető", "CalendarLegendSeriesFinaleTooltip": "Sorozat vagy évad finálé", "CalendarLegendSeriesPremiereTooltip": "Sorozat vagy évad premierje", @@ -356,6 +367,7 @@ "DeleteEpisodeFromDisk": "Epizód törlése a lemezről", "DeleteEpisodesFiles": "{episodeFileCount} epizódfájl törlése", "DeleteEpisodesFilesHelpText": "Törölje az epizódfájlokat és a sorozat mappáját", + "DeleteFiles": "Fájlok törlése", "DeleteImportList": "Importálási lista törlése", "DeleteImportListExclusion": "Importálási lista kizárásának törlése", "DeleteImportListExclusionMessageText": "Biztosan törli ezt az importlista-kizárást?", @@ -383,6 +395,8 @@ "DeleteSelectedIndexers": "Indexelő(k) törlése", "DeleteSelectedIndexersMessageText": "Biztosan törölni szeretne {count} kiválasztott indexelőt?", "DeleteSelectedSeries": "A kiválasztott sorozat törlése", + "DeleteSelectedSeriesFiles": "A kijelölt sorozatfájlok törlése", + "DeleteSeriesFilesConfirmation": "Biztosan törölni szeretné az összes nyomon követett epizódfájlt a {count} kiválasztott sorozatból?", "DeleteSeriesFolder": "Sorozatmappa törlése", "DeleteSeriesFolderConfirmation": "A sorozat könyvtár \"{path}\" és minden tartalma törlésre kerül.", "DeleteSeriesFolderCountConfirmation": "Biztosan törölni szeretne {count} kiválasztott sorozatot?", @@ -556,6 +570,7 @@ "DownloadClientTriblerSettingsDirectoryHelpText": "Választható hely a letöltések elhelyezéséhez, hagyja üresen az alapértelmezett Tribler hely használatához", "DownloadClientTriblerSettingsSafeSeeding": "Biztonságos seedelés", "DownloadClientTriblerSettingsSafeSeedingHelpText": "Engedélyezve csak proxykon keresztül seedel.", + "DownloadClientUTorrentProviderMessage": "A uTorrent múltjában előfordult, hogy kriptobányászót, kártevőket és reklámokat tartalmazott, ezért erősen javasoljuk, hogy válasszon egy másik klienst.", "DownloadClientUTorrentTorrentStateError": "Az uTorrent hibát jelez", "DownloadClientUnavailable": "Letöltőkliens nem elérhető", "DownloadClientValidationApiKeyIncorrect": "Az API kulcs helytelen", @@ -693,6 +708,7 @@ "Events": "Események", "Example": "Példa", "Exception": "Kivétel", + "ExcludeSpecials": "Különkiadások kizárása", "ExcludeUnknownSeriesItems": "Ismeretlen sorozatelemek kizárása", "ExcludedReleaseProfile": "Kizárt kiadási profil", "ExcludedReleaseProfiles": "Kizárt kiadási profilok", @@ -782,6 +798,7 @@ "Formats": "Formátumok", "Forums": "Fórumok", "FreeSpace": "Szabad hely", + "Friday": "Péntek", "From": "tól", "FullColorEvents": "Színes események", "FullColorEventsHelpText": "Módosított stílus: a teljes esemény a státusz színét kapja, nem csak a bal oldali sáv. A Teendők nézetre nem vonatkozik", @@ -877,7 +894,7 @@ "ImportListsAniListSettingsImportDropped": "Importálás elvetve", "ImportListsAniListSettingsImportDroppedHelpText": "Lista: Elvetve", "ImportListsAniListSettingsImportFinished": "Az importálás befejeződött", - "ImportListsAniListSettingsImportFinishedHelpText": "Média: Minden epizódot leadtak", + "ImportListsAniListSettingsImportFinishedHelpText": "Média: Minden epizód adásba került", "ImportListsAniListSettingsImportHiatus": "Importálás szünet", "ImportListsAniListSettingsImportHiatusHelpText": "Média: Szünetelő sorozatok", "ImportListsAniListSettingsImportNotYetReleased": "Az importálás még nem jelent meg", @@ -1094,6 +1111,7 @@ "InstanceName": "Példány neve", "InstanceNameHelpText": "Instancia név a fülön és a Syslog alkalmazásnévhez", "InteractiveImport": "Interaktív Import", + "InteractiveImportDuplicateEpisodes": "Egy vagy több epizód több fájlhoz van társítva", "InteractiveImportLoadError": "Nem sikerült betölteni a kézi importálási elemeket", "InteractiveImportMultipleQueueItems": "Több sorban álló tételek", "InteractiveImportNoEpisode": "Minden kiválasztott fájlhoz legalább egy epizódot ki kell választani", @@ -1168,6 +1186,7 @@ "Logs": "Naplók", "LongDateFormat": "Hosszú dátum formátum", "Lowercase": "Kisbetűs", + "MainNavigation": "Főmenü", "MaintenanceRelease": "Karbantartási kiadás: hibajavítások és egyéb fejlesztések. További részletekért lásd: Github Commit History", "ManageClients": "Ügyfelek kezelése", "ManageCustomFormats": "Egyéni formátumok kezelése", @@ -1268,15 +1287,15 @@ "MonitorEpisodes": "Epizódok figyelése", "MonitorEpisodesModalInfo": "Ez a beállítás csak azt módosítja, hogy egy sorozaton belül mely epizódok vagy évadok legyenek figyelve. A ‘Nincs’ kiválasztása a sorozat figyelésének leállítását eredményezi", "MonitorExistingEpisodes": "Meglévő epizódok", - "MonitorExistingEpisodesDescription": "Figyelje meg azokat az epizódokat, amelyekben vannak fájlok, vagy amelyek még nem kerültek adásba", + "MonitorExistingEpisodesDescription": "Figyelje azokat az epizódokat, amelyeknek vannak fájljai, vagy amelyek még nem kerültek adásba", "MonitorFirstSeason": "Első évad", "MonitorFirstSeasonDescription": "Kövesse nyomon az első évad összes epizódját. Az összes többi évadot figyelmen kívül hagyjuk", "MonitorFutureEpisodes": "Jövőbeni epizódok", - "MonitorFutureEpisodesDescription": "Figyelje meg az adásba még nem került epizódokat", + "MonitorFutureEpisodesDescription": "Figyelje az adásba még nem került epizódokat", "MonitorLastSeason": "Utolsó évad", "MonitorLastSeasonDescription": "Kövesse nyomon az elmúlt évad összes epizódját", "MonitorMissingEpisodes": "Hiányzó epizódok", - "MonitorMissingEpisodesDescription": "Figyelje meg azokat az epizódokat, amelyekhez nem tartoznak fájlok, vagy amelyeket még nem kerültek adásba", + "MonitorMissingEpisodesDescription": "Figyelje azokat az epizódokat, amelyekhez nem tartoznak fájlok, vagy amelyek még nem kerültek adásba", "MonitorNewItems": "Új elemek figyelése", "MonitorNewSeasons": "Kövesse az új évadokat", "MonitorNewSeasonsHelpText": "Mely új évadokat kell automatikusan figyelni", @@ -1607,6 +1626,7 @@ "OrganizeSelectedSeriesModalConfirmation": "Biztos benne hogy rendszerezni kívánja a kiválaszott {count} sorozat összes fájlját?", "OrganizeSelectedSeriesModalHeader": "Kiválasztott sorozatok rendszerezése", "Original": "Eredeti", + "OriginalCountry": "Gyártási ország", "OriginalLanguage": "Eredeti nyelv", "Other": "Egyéb", "OutputPath": "Kimeneti útvonal", @@ -1620,6 +1640,11 @@ "OverviewOptions": "Opciók áttekintése", "PackageVersion": "Csomagverzió", "PackageVersionInfo": "{packageVersion} {packageAuthor}-tól/től", + "PagerGoToFirstPage": "Ugrás az első oldalra", + "PagerGoToLastPage": "Ugrás az utolsó oldalra", + "PagerGoToNextPage": "Ugrás a következő oldalra", + "PagerGoToPage": "Ugrás a(z) {page}. oldalra ({totalPages} oldalból)", + "PagerGoToPreviousPage": "Vissza az előző oldalra", "Parse": "Elemzés", "ParseModalErrorParsing": "Hiba történt az elemzés közben, kérjük, próbálja újra.", "ParseModalHelpText": "Adja meg a kiadás címét a fenti bevitelben", @@ -1688,6 +1713,9 @@ "QualityDefinitionsSizeNotice": "A méretkorlátozások átkerültek a minőségi profilokhoz", "QualityProfile": "Minőségi profil", "QualityProfileInUseSeriesListCollection": "Nem törölhető egy sorozathoz, listához vagy gyűjteményhez csatolt minőségi profil", + "QualityProfileUsage": "Minőségi profil használata", + "QualityProfileUsedInCountImportLists": "{count} importlista használja", + "QualityProfileUsedInCountSeries": "{count} sorozat használja", "QualityProfiles": "Minőségi profilok", "QualityProfilesLoadError": "Nem sikerült betölteni a minőségi profilokat", "QualitySettings": "Minőség Beállítások", @@ -1857,7 +1885,9 @@ "RssSyncInterval": "RSS szinkronizálási intervallum", "RssSyncIntervalHelpText": "Intervallum percekben. A letiltáshoz állítsa nullára (ez leállítja az összes automatikus feloldást)", "RssSyncIntervalHelpTextWarning": "Ez minden indexelőre vonatkozik, kérjük, kövesse az általuk meghatározott szabályokat", + "Run": "Futtatás", "Runtime": "Futási Idő", + "Saturday": "Szombat", "Save": "Mentés", "SaveChanges": "Változtatások mentése", "SaveSettings": "Beállítások mentése", @@ -1906,6 +1936,8 @@ "SeasonPremiere": "Évad Premier", "SeasonPremieresOnly": "Csak az évad premierjei", "Seasons": "Évad", + "SeasonsMonitoredAll": "Mind", + "SeasonsMonitoredNone": "Nincs", "SeasonsMonitoredStatus": "Figyelt évadok", "SecretToken": "Titkos token", "Security": "Biztonság", @@ -1944,6 +1976,7 @@ "SeriesFolderImportedTooltip": "Az epizód a sorozat mappájából importálva", "SeriesFootNote": "Opcionálisan szabályozható a maximális bájtméretre csonkítás, beleértve az ellipszist (...) is. A végéről történő csonkítás (pl. {Sorozatcím:30}) és az elejéről történő csonkítás (pl. {Sorozatcím:-30}) egyaránt támogatott.", "SeriesID": "Sorozat ID", + "SeriesInImportListExclusions": "A sorozat szerepel az importálási lista kizárásai között", "SeriesIndexFooterContinuing": "Folytatás (Minden epizód letöltve)", "SeriesIndexFooterDownloading": "Folytatás (Minden epizód letöltve)", "SeriesIndexFooterEnded": "Befejeződött (az összes epizód letöltve)", @@ -2053,6 +2086,7 @@ "SupportedListsMoreInfo": "Az egyes listákkal kapcsolatos további információkért kattintson a további információ gombokra.", "SupportedListsSeries": "A {appName} több listát is támogat a sorozatok adatbázisba történő importálásához.", "System": "Rendszer", + "SystemDefault": "Rendszer alapértelmezett", "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", "Table": "Táblázat", "TableColumns": "Oszlopok", @@ -2083,9 +2117,11 @@ "Theme": "Téma", "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", "Threshold": "Küszöbérték", + "Thursday": "Csütörtök", "Time": "Idő", "TimeFormat": "Időformátum", "TimeLeft": "Hátralévő idő", + "TimeZone": "Időzóna", "Title": "Cím", "Titles": "Címek", "Today": "Ma", @@ -2114,6 +2150,7 @@ "TotalSpace": "Összes terület", "Trace": "Nyomon követés", "True": "Igaz", + "Tuesday": "Kedd", "TvdbId": "TVDB ID", "TvdbIdExcludeHelpText": "A kizárandó sorozat TVDB azonosítója", "Twitter": "Twitter", @@ -2206,6 +2243,7 @@ "VideoCodec": "Videókodek", "VideoDynamicRange": "Videó dinamikatartomány", "View": "Nézet", + "ViewSeriesOnTvdb": "{title} megtekintése TVDB-n", "VisitTheWikiForMoreDetails": "További részletekért keresse fel a Wikit: ", "WaitingToImport": "Importálásra vár", "WaitingToProcess": "Feldolgozásra vár", @@ -2213,6 +2251,7 @@ "Wanted": "Keresett", "Warn": "Figyelmeztetés", "Warning": "Figyelmeztetés", + "Wednesday": "Szerda", "Week": "Hét", "WeekColumnHeader": "Hét oszlopfejléc", "WeekColumnHeaderHelpText": "Minden oszlop felett jelenjen meg, hogy melyik hét az aktuális", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 6f3426323..a627a2f89 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -13,6 +13,7 @@ "AddConditionError": "Non è stato possibile aggiungere una nuova condizione. Riprova.", "AddConditionImplementation": "Aggiungi Condizione - {implementationName}", "AddConnection": "Aggiungi Connessione", + "AddConnectionError": "Impossibile aggiungere una nuova connessione, riprova.", "AddConnectionImplementation": "Aggiungi Connessione - {implementationName}", "AddCustomFilter": "Aggiungi Filtro Personalizzato", "AddCustomFormat": "Aggiungi Formato Personalizzato", @@ -61,25 +62,29 @@ "AgeWhenGrabbed": "Età (quando recuperato)", "Agenda": "Agenda", "AirDate": "Data di Trasmissione", + "AirDateGracePeriod": "Periodo di Tolleranza per la Data di Trasmissione", + "AirDateGracePeriodHelpText": "Valori negativi permettono di recuperare prima della data di trasmissione, valori positivi impediscono di recuperare dopo la data di trasmissione.", + "AirDateRestriction": "Rifiuta Trasmissioni Inedite", + "AirDateRestrictionHelpText": "Impedisce a {appName} di recuperare uscite che contengono episodi non ancora trasmessi.", "Airs": "Trasmesso", "AirsDateAtTimeOn": "il {date} alle {time} su {networkLabel}", - "AirsTbaOn": "Verrà trasmesso su {networkLabel}", + "AirsTbaOn": "Verrà annunciato su {networkLabel}", "AirsTimeOn": "alle {time} su {networkLabel}", "AirsTomorrowOn": "Domani alle {time} su {networkLabel}", "All": "Tutti", "AllFiles": "Tutti i File", "AllResultsAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro applicato", - "AllSeriesAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro", + "AllSeriesAreHiddenByTheAppliedFilter": "Tutti i risultati sono nascosti dal filtro applicato", "AllSeriesInRootFolderHaveBeenImported": "Tutte le serie in {path} sono state importate", "AllTitles": "Tutti i Titoli", "AlreadyInYourLibrary": "Già presente nella tua libreria", - "AlternateTitles": "Titolo alternativo", + "AlternateTitles": "Titoli alternativi", "Always": "Sempre", - "AnEpisodeIsDownloading": "Un episodio è in download", + "AnEpisodeIsDownloading": "Un Episodio è in download", "AnalyseVideoFiles": "Analizza i file video", "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.", "Analytics": "Statistiche", - "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.", + "AnalyticsEnabledHelpText": "Invia 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.", "Anime": "Anime", "AnimeEpisodeFormat": "Formato Episodi Anime", "AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto", @@ -131,6 +136,7 @@ "AutoTaggingSpecificationMaximumYear": "Anno Massimo", "AutoTaggingSpecificationMinimumYear": "Anno Minimo", "AutoTaggingSpecificationNetwork": "Rete(i)", + "AutoTaggingSpecificationOriginalCountry": "Nazione", "AutoTaggingSpecificationOriginalLanguage": "Lingua", "AutoTaggingSpecificationQualityProfile": "Profilo Qualità", "AutoTaggingSpecificationRootFolder": "Cartella Radice", @@ -166,8 +172,10 @@ "BlocklistRelease": "Release in Lista dei Blocchi", "BlocklistReleaseHelpText": "Impedisci a {appName} di scaricare nuovamente questa release via RSS o Ricerca Automatica", "BlocklistReleases": "Blocca questa Release", - "Branch": "Branca", - "BranchUpdate": "Branca da usare per aggiornare {appName}", + "Blocklisted": "In Blocklist", + "BlocklistedAt": "In Blocklist dal {date}", + "Branch": "Ramo", + "BranchUpdate": "Ramo da usare per aggiornare {appName}", "BranchUpdateMechanism": "Ramo utilizzato dal sistema di aggiornamento esterno", "BrowserReloadRequired": "Richiede il reload del Browser", "BuiltIn": "Incluso", @@ -176,7 +184,7 @@ "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Punteggio minimo del Formato Personalizzato", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Punteggio minimo del Formato Personalizzato per ignorare il ritardo del protocollo preferito", "BypassDelayIfHighestQuality": "Ignora se è alla qualità massima", - "BypassDelayIfHighestQualityHelpText": "Ignora il ritardo quando la release ha la massima qualità abilitata nel profilo qualità con il protocollo preferito", + "BypassDelayIfHighestQualityHelpText": "Ignora il ritardo quando l'uscita ha la massima qualità abilitata nel profilo qualità con il protocollo preferito", "BypassProxyForLocalAddresses": "Ignora il Proxy per Indirizzi Locali", "Calendar": "Calendario", "CalendarFeed": "Feed calendario {appName}", @@ -220,7 +228,7 @@ "ClickToChangeLanguage": "Click per cambiare lingua", "ClickToChangeQuality": "Click per cambiare qualità", "ClickToChangeReleaseGroup": "Clicca per cambiare gruppo di rilascio", - "ClickToChangeReleaseType": "Clicca per cambiare il tipo di release", + "ClickToChangeReleaseType": "Clicca per cambiare il tipo di uscita", "ClickToChangeSeason": "Click per cambiare stagione", "ClickToChangeSeries": "Click per cambiare serie", "ClientPriority": "Priorità Client", @@ -253,6 +261,7 @@ "ConnectionLostToBackend": "{appName} ha perso la connessione al backend e dovrà essere ricaricato per ripristinare la funzionalità.", "ConnectionSettingsUrlBaseHelpText": "Aggiunge un prefisso all'url della {connectionName}, come {url}", "Connections": "Connessioni", + "ConnectionsLoadError": "Impossibile caricare le Connessioni", "Continuing": "In Corso", "ContinuingOnly": "Solo In Corso", "ContinuingSeriesDescription": "Altri episodi/stagioni sono attesi", @@ -274,20 +283,25 @@ "CreateGroup": "Crea gruppo", "CurrentlyInstalled": "Attualmente Installato", "Custom": "Personalizzato", + "CustomColonReplacement": "Sostituzione Personalizzata Due Punti", "CustomColonReplacementFormatHelpText": "Caratteri da utilizzare al posto dei due punti", + "CustomColonReplacementFormatHint": "Carattere valido del file system come i Due Punti (Lettera)", "CustomFilter": "Filtro Personalizzato", "CustomFilters": "Filtri Personalizzati", "CustomFormat": "Formato Personalizzato", "CustomFormatHelpText": "{appName} valuta ogni release usando la somma dei punteggi dei corrispondenti formati personalizzati. Se una nuova versione migliorasse il punteggio, con una qualità uguale o migliore, {appName} lo prenderà.", "CustomFormatJson": "Formato Personalizzato JSON", "CustomFormatScore": "Formato Personalizzato Punteggio", + "CustomFormatUnknownCondition": "Condizione del Formato Personalizzato '{implementation}' sconosciuta", "CustomFormatUnknownConditionOption": "Opzione sconosciuta '{key}' per la condizione '{implementation}'", "CustomFormats": "Formati Personalizzati", "CustomFormatsLoadError": "Impossibile a caricare Formati Personalizzati", "CustomFormatsSettings": "Formati Personalizzati Impostazioni", "CustomFormatsSettingsSummary": "Formati Personalizzati Impostazioni", + "CustomFormatsSettingsTriggerInfo": "Un Formato Personalizzato sarà applicato ad un'uscita o a un file quando corrisponde ad almeno una delle diverse tipologie di condizioni selezionate.", "CustomFormatsSpecificationExceptLanguage": "Escludi lingua", "CustomFormatsSpecificationExceptLanguageHelpText": "Si applica se qualsiasi lingua diversa da quella selezionata è presente", + "CustomFormatsSpecificationFlag": "Flag", "CustomFormatsSpecificationLanguage": "Lingua", "CustomFormatsSpecificationMaximumSize": "Dimensione Massima", "CustomFormatsSpecificationMaximumSizeHelpText": "La release deve essere minore o uguale a questa dimensione", @@ -295,7 +309,7 @@ "CustomFormatsSpecificationMinimumSizeHelpText": "La release deve essere maggiore di questa dimensione", "CustomFormatsSpecificationRegularExpression": "Espressione Regolare", "CustomFormatsSpecificationRegularExpressionHelpText": "L'espressione regolare del Formato Personalizzato ignora le maiuscole/minuscole", - "CustomFormatsSpecificationReleaseGroup": "Gruppo Release", + "CustomFormatsSpecificationReleaseGroup": "Gruppo Uscita", "CustomFormatsSpecificationResolution": "Risoluzione", "CustomFormatsSpecificationSource": "Fonte", "Cutoff": "Soglia", @@ -350,7 +364,10 @@ "DeleteEpisodeFromDisk": "Cancella episodio dal disco", "DeleteEpisodesFiles": "Elimina i File di {episodeFileCount} Episodi", "DeleteEpisodesFilesHelpText": "Cancella i file degli episodi e la cartella della serie", + "DeleteFiles": "Elimina File", "DeleteImportList": "Cancella la lista di importazione", + "DeleteImportListExclusion": "Rimuovi Esclusione dalla Lista Importazioni", + "DeleteImportListExclusionMessageText": "Sei sicuro di voler rimuovere questa esclusione dalla lista di esclusioni delle importazioni?", "DeleteImportListMessageText": "Sei sicuro di voler eliminare la lista '{name}'?", "DeleteIndexer": "Cancella Indice", "DeleteIndexerMessageText": "Sei sicuro di voler eliminare l'indice '{name}'?", @@ -359,9 +376,12 @@ "DeleteQualityProfile": "Elimina Profilo Qualità", "DeleteQualityProfileMessageText": "Sicuro di voler cancellare il profilo di qualità '{name}'?", "DeleteReleaseProfile": "Cancellare il profilo release", + "DeleteReleaseProfileMessageText": "Sicuro di voler eliminare il profilo uscita '{name}'?", + "DeleteRemotePathMapping": "Elimina la Mappatura dei Percorsi Remoti", "DeleteSelectedDownloadClients": "Cancella i Client di Download", "DeleteSelectedDownloadClientsMessageText": "Sei sicuro di voler eliminare i '{count}' client di download selezionato/i?", "DeleteSelectedEpisodeFiles": "Elimina i File degli Episodi Selezionati", + "DeleteSelectedImportLists": "Cancella la(e) lista(e) di importazione", "DeleteSelectedImportListsMessageText": "Confermi di voler eliminare le {count} liste di importazione selezionate?", "DeleteSelectedIndexers": "Elimina Indice/i", "DeleteSelectedIndexersMessageText": "Confermi di voler eliminare i {count} indici selezionati?", @@ -369,6 +389,8 @@ "Deleted": "Cancellato", "Destination": "Destinazione", "DetailedProgressBarHelpText": "Mostra testo sulla barra di avanzamento", + "Details": "Dettagli", + "Disabled": "Disabilitato", "Discord": "Discord", "DiskSpace": "Spazio sul Disco", "Docker": "Docker", @@ -376,6 +398,7 @@ "Donate": "Dona", "Donations": "Donazioni", "DotNetVersion": ".NET", + "Download": "Scarica", "DownloadClient": "Client di Download", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Non è disponibile nessun client di download", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}. {errorMessage}", @@ -411,6 +434,7 @@ "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Categorie non supportate fino alla versione 3.3.0 di qBittorrent. Per favore aggiorna o prova con una Categoria vuota.", "DownloadClientRTorrentSettingsAddStopped": "Aggiungi Fermato", "DownloadClientRTorrentSettingsUrlPath": "Percorso Url", + "DownloadClientRootFolderHealthCheckMessage": "Il client di download {downloadClientName} colloca i download nella cartella radice {rootFolderPath}. Non dovresti scaricare in una cartella radice.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Disattiva ordinamento per data", "DownloadClientSabnzbdValidationUnknownVersion": "Versione sconosciuta: {rawVersion}", "DownloadClientSettingsAddPaused": "Aggiungi In Pausa", @@ -438,6 +462,7 @@ "DownloadClientValidationUnknownException": "Eccezione sconosciuta: {exception}", "DownloadClientValidationVerifySsl": "Verifica impostazioni SSL", "DownloadClientValidationVerifySslDetail": "Per favore verifica la tua configurazione SSL su entrambi {clientName} e {appName}", + "DownloadClients": "Client di Download", "DownloadFailed": "Download Fallito", "DownloadFailedEpisodeTooltip": "Download dell'episodio fallito", "DownloadIgnored": "Download Ignorato", @@ -485,21 +510,29 @@ "EpisodeFileRenamedTooltip": "Episodio del file rinominato", "EpisodeImported": "Episodio Importato", "EpisodeInfo": "Info Episodio", + "EpisodeNumbers": "Numero(i) di Episodio", "EpisodeRequested": "Episodio Richiesto", "EpisodeTitle": "Titolo Episodio", "EpisodeTitleRequired": "Titolo Episodio Richiesto", "Episodes": "Episodi", "Error": "Errore", "ErrorLoadingContent": "Si è verificato un errore caricando questo contenuto", + "ErrorRestoringBackup": "Errore durante il ripristino del backup", "Events": "Eventi", "Example": "Esempio", "Exception": "Eccezione", "Existing": "Esistente", + "ExistingTag": "Etichetta esistente", + "ExportCustomFormat": "Esporta formato personalizzato", "External": "Esterno", + "ExternalUpdater": "{appName} è configurato per utilizzare un meccanismo di aggiornamento esterno", "Failed": "Fallito", + "FailedToFetchUpdates": "Impossibile recuperare aggiornamenti", "False": "Falso", + "FeatureRequests": "Richieste di funzionalità", "File": "File", "FileManagement": "Gestione File", + "Filename": "Nome del File", "Files": "File", "Filter": "Filtro", "FilterContains": "contiene", @@ -523,6 +556,7 @@ "Filters": "Filtri", "FinaleTooltip": "Serie o finale di stagione", "FirstDayOfWeek": "Primo Giorno della Settimana", + "Fixed": "Corretto", "Folder": "Cartella", "Folders": "Cartelle", "FormatAgeDay": "giorno", @@ -540,8 +574,13 @@ "FormatShortTimeSpanSeconds": "{seconds} secondo/i", "FormatTimeSpanDays": "{days}d {time}", "Formats": "Formati", + "Forums": "Forum", "FreeSpace": "Spazio Libero", "From": "Da", + "FullSeason": "Stagione Intera", + "General": "Generale", + "GeneralSettings": "Impostazioni Generali", + "Health": "Salute", "HiddenClickToShow": "Nascosto, premi per mostrare", "HideAdvanced": "Nascondi Avanzate", "History": "Storico", @@ -558,6 +597,7 @@ "ICalLink": "Link iCal", "ICalShowAsAllDayEvents": "Mostra come eventi di tutta la giornata", "ICalShowAsAllDayEventsHelpText": "Gli eventi appariranno come eventi di un giorno intero nel tuo calendario", + "IRC": "IRC", "IRCLinkText": "#sonarr su Libera", "IconForCutoffUnmet": "Icona per Soglia non raggiunta", "IconForCutoffUnmetHelpText": "Mostra un'icona per i file che non raggiungono la soglia", @@ -565,9 +605,11 @@ "IgnoreDownloads": "Ignora Download", "Implementation": "Implementazione", "ImportListRootFolderMissingRootHealthCheckMessage": "Persa la cartella principale per l’importazione delle liste : {rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Diverse cartelle radici sono mancanti per le liste di importazione: {rootFolderInfo}", "ImportListSettings": "Impostazioni delle Liste", "ImportListStatusAllUnavailableHealthCheckMessage": "Tutte le liste non sono disponibili a causa di errori", "ImportListStatusUnavailableHealthCheckMessage": "Liste non disponibili a causa di errori: {importListNames}", + "ImportLists": "Liste di Importazione", "ImportListsSonarrSettingsFullUrl": "URL Completo", "ImportListsTraktSettingsAdditionalParameters": "Parametri Addizionali", "ImportListsTraktSettingsAuthenticateWithTrakt": "Autentica con Trakt", @@ -576,6 +618,9 @@ "ImportListsTraktSettingsListName": "Nome Lista", "ImportListsTraktSettingsListType": "Tipo Lista", "ImportListsTraktSettingsRating": "Valutazione", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Abilita Gestione dei Download Completati se possibile", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Abilita la Gestione dei Download Completati se possibile (Multi-Computer non supportato)", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Abilita la Gestione dei Download Completati", "ImportSeries": "Importa Serie", "Imported": "Importato", "Importing": "Importando", @@ -585,6 +630,7 @@ "IndexerHDBitsSettingsCategories": "Categorie", "IndexerHDBitsSettingsCodecsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", "IndexerHDBitsSettingsMediumsHelpText": "Se non specificato, saranno utilizzate tutte le opzioni.", + "IndexerJackettAllHealthCheckMessage": "Indici che usano l'endpoint non supportato 'all' di Hackett: {indexerNames}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Nessun indice è disponibile da più di 6 ore a causa di errori", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Alcuni indici non sono disponibili da più di 6 ore a causa di errori: {indexerNames}", "IndexerOptionsLoadError": "Impossibile caricare le opzioni dell'indice", @@ -609,23 +655,39 @@ "Indexers": "Indici", "IndexersLoadError": "Impossibile caricare gli Indici", "InfoUrl": "URL Info", + "InstallLatest": "Installa il più Recente", "InteractiveImportNoFilesFound": "Nessun video trovato nella castella selezionata", "InteractiveImportNoImportMode": "Una modalità di importazione deve essere selezionata", "InteractiveImportNoQuality": "Una qualità deve essere scelta per ogni file selezionato", + "Interval": "Intervallo", "InvalidUILanguage": "L'interfaccia è impostata in una lingua non valida, correggi e salva le tue impostazioni", "LabelIsRequired": "Etichetta richiesta", "Language": "Lingua", "Languages": "Lingue", "LanguagesLoadError": "Impossibile caricare le lingue", + "LastDuration": "Ultima Durata", + "LastExecution": "Ultima esecuzione", + "LastWriteTime": "Orario di Ultima Scrittura", "LiberaWebchat": "Chat web Libera", + "LibraryImport": "Importazione Libreria", "ListWillRefreshEveryInterval": "Le liste verranno aggiornate ogni {refreshInterval}", + "Location": "Posizione", + "LogFiles": "File di Log", "LogFilesLocation": "File di Log localizzati in: {location}", + "Logs": "Registri", + "MaintenanceRelease": "Release di Manutenzione: bug fix e altri miglioramenti. Vedi la storia dei commit Github per maggiori dettagli", "ManageClients": "Gestisci Clients", "ManageDownloadClients": "Gestisci Clients di Download", + "ManageImportLists": "Gestisci Liste di Importazione", "ManageIndexers": "Gestisci Indici", "ManageLists": "Gestisci Liste", + "Manual": "Manuale", "MassSearchCancelWarning": "Questa non può essere annullata una volta avviata senza riavviare {appName} o disattivando tutti i tuoi indici.", + "MatchedToEpisodes": "Abbinato agli Episodi", + "MatchedToSeason": "Abbinato alla Stagione", + "MatchedToSeries": "Abbinato alla Serie", "MaximumSize": "Dimensione Massima", + "MediaManagement": "Gestione Media", "Message": "Messaggio", "Metadata": "Metadati", "MetadataLoadError": "Impossibile caricare i Metadati", @@ -684,6 +746,7 @@ "MoveFiles": "Sposta File", "MoveSeriesFoldersDontMoveFiles": "No, Sposterò i File da Solo", "MoveSeriesFoldersMoveFiles": "Sì, Sposta i File", + "MultiSeason": "Multi Stagione", "MustContain": "Deve Contenere", "MustContainHelpText": "La release deve contenere almeno uno di questi termini (senza distinzione tra maiuscole e minuscole)", "MustNotContain": "Non Deve Contenere", @@ -694,6 +757,8 @@ "Network": "Rete", "Never": "Mai", "New": "Nuovo", + "NextAiring": "Prossima Trasmissione", + "NextExecution": "Prossima esecuzione", "No": "No", "NoBackupsAreAvailable": "Nessun backup disponibile", "NoChange": "Nessun Cambio", @@ -702,8 +767,12 @@ "NoEpisodesInThisSeason": "Nessun episodio in questa stagione", "NoEventsFound": "Nessun evento trovato", "NoHistoryFound": "Nessun storico trovato", + "NoImportListsFound": "Nessuna lista di importazione trovata", "NoIndexersFound": "Nessun indice trovato", + "NoIssuesWithYourConfiguration": "La tua configurazione non presenta problemi", + "NoLeaveIt": "No, Lascialo", "NoLogFiles": "Nessun file di log", + "NoSeasons": "Nessuna stagione", "NoUpdatesAreAvailable": "Nessun aggiornamento disponibile", "NotificationsCustomScriptSettingsName": "Script personalizzato", "NotificationsCustomScriptValidationFileDoesNotExist": "File non esiste", @@ -763,6 +832,7 @@ "NotificationsValidationUnableToSendTestMessageApiResponse": "Impossibile inviare messaggio di prova. Risposta dalle API: {error}", "OnLatestVersion": "L'ultima versione di {appName} è già installata", "OneMinute": "1 Minuto", + "OneSeason": "1 Stagione", "OnlyTorrent": "Solo Torrent", "OnlyUsenet": "Solo Usenet", "OpenBrowserOnStart": "Apri browser all'avvio", @@ -810,6 +880,8 @@ "Preferred": "Preferito", "PreferredProtocol": "Protocollo Preferito", "PreferredSize": "Dimensione Preferita", + "PrefixedRange": "Intervallo Prefisso", + "PreviousAiring": "Trasmissione Precedente", "PreviouslyInstalled": "Precedentemente Installato", "Priority": "Priorità", "PrioritySettings": "Priorità: {priority}", @@ -817,11 +889,13 @@ "ProfilesSettingsSummary": "Profili di Qualità, Lingua, Ritardo e Release", "Progress": "Progressi", "ProgressBarProgress": "Barra Progressi al {progress}%", + "Proper": "Proper", "Protocol": "Protocollo", "Proxy": "Proxy", "ProxyBadRequestHealthCheckMessage": "Test del proxy fallito: Status Code: {statusCode}", "ProxyBypassFilterHelpText": "Usa ',' come separatore, e '*.' come wildcard per i sottodomini", "ProxyFailedToTestHealthCheckMessage": "Test del proxy fallito: {url}", + "ProxyResolveIpHealthCheckMessage": "Impossibile risolvere l'indirizzo IP per l'Host Configurato del Proxy {proxyHostName}", "ProxyType": "Tipo Proxy", "PublishedDate": "Data Pubblicazione", "Qualities": "Qualità", @@ -841,6 +915,7 @@ "Real": "Reale", "Reason": "Ragione", "RecentChanges": "Cambiamenti Recenti", + "RecycleBinUnableToWriteHealthCheckMessage": "Impossibile scrivere nella cartella cestino configurata: {path}. Assicurarsi che questo percorso esista e che sia scrivibile dall'utente che esegue {appName}", "RecyclingBin": "Cestino", "RecyclingBinCleanup": "Pulizia Cestino", "RecyclingBinCleanupHelpText": "Imposta a 0 per disattivare la pulizia automatica", @@ -848,8 +923,28 @@ "RefreshAndScan": "Aggiorna & Scansiona", "RefreshAndScanTooltip": "Aggiorna informazioni e scansiona disco", "RefreshSeries": "Aggiorna Serie", + "Release": "Uscita", + "ReleaseGroup": "Gruppo Uscita", + "ReleaseHash": "Hash di Uscita", + "ReleaseTitle": "Titolo Uscita", "Reload": "Ricarica", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Stai usando docker; il client di download {downloadClientName} inserisce gli scaricamenti in {path} ma questo non è un percorso valido in {osName}. Controlla la mappatura dei percorsi remoti e le impostazioni del client di download.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Stai usando docker; il client di download {downloadClientName} inserisce gli scaricamenti in {path} ma questa cartella non sembra essere presente nel container. Controlla la mappatura dei percorsi remoti e le impostazioni dei volumi del container.", + "RemotePathMappingFileRemovedHealthCheckMessage": "Il file {path} è stato rimosso durante l'elaborazione.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Stai usando docker; il client di download {downloadClientName} ha segnalato i file in {path} ma questo non è un percorso valido in {osName}. Controlla la mappatura dei percorsi remoti e le impostazioni del client di download.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Il client di download {downloadClientName} ha segnalato i file in {path} ma {appName} non trova questo percorso. Potrebbe essere necessario modificare i permessi della cartella.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Il client di download locale {downloadClientName} riporta files in {path} ma questo non è un percorso valido in {osName}. Controlla le impostazioni del client di download.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Il client di download remoto {downloadClientName} riporta files in {path} ma questo non è un percorso valido in {osName}. Controlla la mappatura dei percorsi remoto e le impostazioni del client di download.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} riesce a vedere ma non ad accedere alla cartella di download {downloadPath}. Probabilmente un errore di permessi.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Il client di download {downloadClientName} inserisce i file scaricati in {path} ma {appName} non riesce a vedere questo percorso. Potrebbe essere necessario modificare i permessi della cartella.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Il client di download remoto {downloadClientName} inserisce gli scaricamenti in {path} ma questa cartella non sembra esistere. Probabilmente la mappatura dei percorsi remoti è incorretta o mancante.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Il client di download locale {downloadClientName} inserisce gli scaricamenti in {path} ma questo non è un percorso valido in {osName}. Controlla le impostazioni del client di download.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Il client di download remoto {downloadClientName} riporta files in {path} ma questa cartella non sembra esistere. Probabilmente manca la mappatura dei percorsi remoti.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Il client di download remoto {downloadClientName} riporta files in {path} ma questo non è un percorso valido in {osName}. Controlla le impostazioni del client di download.", + "Remove": "Rimuovi", + "RemoveCompleted": "Rimuovi completati", "RemoveCompletedDownloads": "Rimuovi Download Completati", + "RemoveFailed": "Rimuovi Falliti", "RemoveFailedDownloads": "Rimuovi Download Falliti", "RemoveFromDownloadClient": "Rimuovi dal client di download", "RemoveFromDownloadClientHint": "Rimuovi il download e i file dal client di download", @@ -857,9 +952,17 @@ "RemoveQueueItem": "Rimuovi - {sourceTitle}", "RemoveQueueItemConfirmation": "Sei sicuro di voler rimuovere '{sourceTitle}' dalla coda?", "RemoveQueueItemRemovalMethod": "Metodo di Rimozione", + "RemoveSelectedItem": "Rimuovi elemento selezionato", "RemoveSelectedItemQueueMessageText": "Sei sicuro di voler rimuovere 1 elemento dalla coda?", + "RemoveSelectedItems": "Rimuovi elementi selezionati", "RemoveSelectedItemsQueueMessageText": "Sei sicuro di voler rimuovere {selectedCount} elementi dalla coda?", "RemovedFromTaskQueue": "Rimosso dalla coda", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Le serie {series} sono state rimosse da TheTVDB", + "RemovedSeriesSingleRemovedHealthCheckMessage": "La serie {series} è stata rimossa da TheTVDB", + "RemovingTag": "Eliminando l'etichetta", + "Repack": "Repack", + "Replace": "Sostituisci", + "Required": "Necessario", "RequiredHelpText": "Questa condizione per {implementationName} deve corrispondere perché si applichi il formato personalizzato. Altrimenti, è sufficiente una singola corrispondenza tra quelle per {implementationName}.", "Reset": "Reimposta", "ResetQualityDefinitions": "Reimposta definizioni delle Qualità", @@ -875,6 +978,10 @@ "RestrictionsLoadError": "Impossibile caricare le Restrizioni", "Result": "Risultato", "RetryingDownloadOn": "Riprovando il download il {date} alle {time}", + "RootFolder": "Cartella Radice", + "RootFolderMissingHealthCheckMessage": "Cartella radice mancante: {rootFolderPath}", + "RootFolderMultipleMissingHealthCheckMessage": "Ci sono più cartelle radice mancanti: {rootFolderPaths}", + "RootFolderPath": "Percorso Cartella Radice", "RootFolderSelectFreeSpace": "{freeSpace} Libero", "Rss": "RSS", "RssIsNotSupportedWithThisIndexer": "RSS non supportato con questo indice", @@ -943,6 +1050,7 @@ "SeriesTypes": "Tipi Serie", "SetIndexerFlags": "Configura Etichette dell'Indice", "SetPermissions": "Imposta Permessi", + "SetTags": "Imposta Etichette", "Settings": "Impostazioni", "ShowAdvanced": "Mostra Avanzate", "ShowDateAdded": "Mostra Data Aggiunta", @@ -952,6 +1060,7 @@ "ShowNetwork": "Mostra Rete", "ShowPath": "Mostra Percorso", "ShowTitle": "Mostra Titolo", + "ShownClickToHide": "Visibile, clicca per nascondere", "Shutdown": "Spegnimento", "SingleEpisode": "Episodio Singolo", "Size": "Dimensione", @@ -978,6 +1087,7 @@ "SubtitleLanguages": "Lingue dei Sottotitoli", "Sunday": "Domenica", "System": "Sistema", + "SystemTimeHealthCheckMessage": "L'orario di sistema è sbagliato di più di un giorno. Le attività pianificate potrebbero non essere eseguite correttamente fino alla correzione", "Table": "Tabella", "TableColumns": "Colonne", "TableOptions": "Opzioni Tabella", @@ -986,11 +1096,14 @@ "TablePageSizeMaximum": "La dimensione della pagina non deve superare {maximumValue}", "TablePageSizeMinimum": "La dimensione della pagina deve essere almeno {minimumValue}", "TagDetails": "Dettagli Etichetta - {label}", + "Tags": "Etichette", "TaskUserAgentTooltip": "User-Agent fornito dalla app che ha chiamato la API", + "Tasks": "Attività", "Tba": "TBA", "Test": "Prova", "TestAll": "Prova Tutto", "TestAllIndexers": "Prova tutti gli Indici", + "TestParsing": "Prova l'Analisi", "TheLogLevelDefault": "Il livello di log predefinito è 'Debug' e può essere modificato nelle [Impostazioni Generali](settings/general)", "TheTvdb": "TheTVDB", "Theme": "Tema", @@ -1029,6 +1142,7 @@ "UnknownDownloadState": "Stato download sconosciuto: {state}", "UnknownEventTooltip": "Evento sconosciuto", "Unlimited": "Illimitato", + "Unmonitored": "Non Monitorato", "UnsavedChanges": "Cambiamenti Non Salvati", "UnselectAll": "Deseleziona Tutto", "Upcoming": "In arrivo", @@ -1036,6 +1150,8 @@ "UpdateAppDirectlyLoadError": "Impossibile aggiornare {appName} direttamente,", "UpdateAvailableHealthCheckMessage": "Aggiornamento disponibile: {version}", "UpdateMechanismHelpText": "Usa il sistema di aggiornamento incorporato di {appName} o uno script", + "UpdateStartupNotWritableHealthCheckMessage": "Impossibile installare l'aggiornamento perché l'utente '{userName}' non ha i permessi di scrittura per la cartella di avvio '{startupFolder}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Impossibile installare l'aggiornamento perché la cartella '{startupFolder}' si trova in una cartella di \"App Translocation\".", "Updates": "Aggiornamenti", "Uppercase": "Maiuscolo", "Uptime": "Tempo di attività", diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index 37d44653d..72033ad40 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -13,7 +13,7 @@ "AddConditionError": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", "AddConditionImplementation": "Legg til betingelse - {implementationName}", "AddConnection": "Legg til tilkobling", - "AddConnectionImplementation": "Legg til tilkobling - {implementationName}", + "AddConnectionImplementation": "Legg til betingelse - {implementationName}", "AddCustomFilter": "Legg til eget filter", "AddCustomFormat": "Nytt Egendefinert format", "AddCustomFormatError": "Kunne ikke legge til nytt egendefinert format, vennligst prøv på nytt.", @@ -52,8 +52,21 @@ "Age": "Alder", "Agenda": "Agenda", "AllTitles": "Alle titler", + "AnalyseVideoFilesHelpText": "Trekke ut informasjon som oppløsning, kjøretid og kodek informasjon fra filer. Dette forutsetter att {appName}leser deler av filen. dette kan forutsake høy disk eller nettverks aktivitet når filer skannes.", + "Any": "Hvilken som helst", "ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {length} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen", + "AppDataDirectory": "AppData Katalog", + "AppUpdated": "{appName} Oppdatert", + "ApplicationUrlHelpText": "Denne applikasjonens eksterne URL inkludert http(s)://, port og URL base", "ApplyChanges": "Bekreft endringer", + "AudioLanguages": "Flerspråklig", + "AuthenticationMethodHelpTextWarning": "Vennligst velg en valid autentiserings metode.", + "AuthenticationRequired": "Verefisering påkrevd", + "AuthenticationRequiredHelpText": "Endre hvilke forespørsler som krever autentisering. Ikke endre dette med mindre du forstår risikoen.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Gjenta nytt passord", + "AuthenticationRequiredPasswordHelpTextWarning": "Oppgi nytt passord", + "AuthenticationRequiredUsernameHelpTextWarning": "Oppgi nytt bruernavn", + "AuthenticationRequiredWarning": "For å forhindre ekstern tilgang uten pålogging, krever {appName} nå at autentisering er aktivert. Du kan velge å deaktivere autentisering for lokale adresser.", "AutomaticAdd": "Legg til automatisk", "CalendarOptions": "Kalenderinnstillinger", "ClearBlocklistMessageText": "Er du sikker på at du vil fjerne alle elementer fra blokkeringslisten?", diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index 1a6821dc5..687370de1 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -212,6 +212,10 @@ "ClearBlocklist": "Blokkeerlijst wissen", "ClearBlocklistMessageText": "Weet je zeker dat je de blokkeerlijst wil legen?", "ClickToChangeIndexerFlags": "Klik om indexeringsvlaggen te wijzigen", + "ClickToChangeQuality": "Klik om de kwaliteit aan te passen", + "ClickToChangeSeason": "Klik om seizoen te veranderen", + "ClickToChangeSeries": "Klik om de serie te veranderen", + "ClientPriority": "Client prioriteit", "Clone": "Kloon", "CloneAutoTag": "Kopieer Automatische Tag", "CloneCondition": "Kloon Conditie", diff --git a/src/NzbDrone.Core/Localization/Core/pl.json b/src/NzbDrone.Core/Localization/Core/pl.json index 21449a285..d19221af0 100644 --- a/src/NzbDrone.Core/Localization/Core/pl.json +++ b/src/NzbDrone.Core/Localization/Core/pl.json @@ -1,98 +1,2265 @@ { - "About": "Informacje", - "Absolute": "Absolutny", - "AbsoluteEpisodeNumber": "Absolutny Numer Odcinka", - "AbsoluteEpisodeNumbers": "Absolutne Numery Odcinków", + "About": "O programie", + "Absolute": "Bezwzględny", + "AbsoluteEpisodeNumber": "Bezwzględny numer odcinka", + "AbsoluteEpisodeNumbers": "Bezwzględny numer(y) odcinka", "Actions": "Akcje", "Activity": "Aktywność", "Add": "Dodaj", "AddANewPath": "Dodaj nową ścieżkę", - "AddAutoTag": "Dodaj automatyczne tagi", - "AddAutoTagError": "Nie można dodać nowego tagu automatycznego, spróbuj ponownie.", + "AddAutoTag": "Dodaj automatyczny tag", + "AddAutoTagError": "Nie można dodać nowego automatycznego tagu, spróbuj ponownie.", "AddCondition": "Dodaj warunek", "AddConditionError": "Nie można dodać nowego warunku, spróbuj ponownie.", - "AddConditionImplementation": "Dodaj condition - {implementationName}", + "AddConditionImplementation": "Dodaj warunek - {implementationName}", "AddConnection": "Dodaj połączenie", - "AddConnectionImplementation": "Dodaj Connection - {implementationName}", + "AddConnectionError": "Nie można dodać nowego połączenia, spróbuj ponownie.", + "AddConnectionImplementation": "Dodaj połączenie - {implementationName}", "AddCustomFilter": "Dodaj niestandardowy filtr", "AddCustomFormat": "Dodaj format niestandardowy", "AddCustomFormatError": "Nie można dodać nowego formatu niestandardowego, spróbuj ponownie.", "AddDelayProfile": "Dodaj profil opóźnienia", - "AddDelayProfileError": "Nie można dodać nowego profilu opóźnienia, spróbuj później.", + "AddDelayProfileError": "Nie można dodać nowego profilu opóźnienia, spróbuj ponownie.", "AddDownloadClient": "Dodaj klienta pobierania", "AddDownloadClientError": "Nie można dodać nowego klienta pobierania, spróbuj ponownie.", "AddDownloadClientImplementation": "Dodaj klienta pobierania - {implementationName}", - "AddExclusion": "Dodaj wyjątek", + "AddExclusion": "Dodaj wykluczenie", "AddImportList": "Dodaj listę importu", "AddImportListExclusion": "Dodaj wykluczenie listy importu", - "AddImportListExclusionError": "Nie można dodać nowego wykluczenia listy, spróbuj ponownie.", - "AddImportListImplementation": "Dodaj Listę Importu - {implementationName}", + "AddImportListExclusionError": "Nie można dodać nowego wykluczenia listy importu, spróbuj ponownie.", + "AddImportListImplementation": "Dodaj listę importu - {implementationName}", "AddIndexer": "Dodaj indekser", - "AddIndexerError": "Nie można dodać nowego indeksatora, spróbuj ponownie.", - "AddIndexerImplementation": "Dodaj indeks - {implementationName}", + "AddIndexerError": "Nie można dodać nowego indeksera, spróbuj ponownie.", + "AddIndexerImplementation": "Dodaj indekser - {implementationName}", "AddList": "Dodaj listę", "AddListError": "Nie można dodać nowej listy, spróbuj ponownie.", - "AddListExclusion": "Dodaj wykluczenie z listy", + "AddListExclusion": "Dodaj wykluczenie listy", "AddListExclusionError": "Nie można dodać nowego wykluczenia listy, spróbuj ponownie.", - "AddNew": "Dodaj nowy", + "AddListExclusionSeriesHelpText": "Zapobiegaj dodawaniu seriali do {appName} przez listy", + "AddNew": "Dodaj nowe", "AddNewRestriction": "Dodaj nowe ograniczenie", "AddNewSeries": "Dodaj nowy serial", - "AddNewSeriesError": "Nie udało się załadować wyników wyszukiwania, spróbuj ponownie.", - "AddNewSeriesHelpText": "Latwo dodać nowy serial, po prostu zacznij pisać nazwę serialu który chcesz dodać.", - "AddNewSeriesSearchForMissingEpisodes": "Zacznij szukać brakujących odcinków", + "AddNewSeriesError": "Nie udało się wczytać wyników wyszukiwania, spróbuj ponownie.", + "AddNewSeriesHelpText": "Dodanie nowego serialu jest proste, zacznij wpisywać nazwę serialu, który chcesz dodać.", + "AddNewSeriesRootFolderHelpText": "Podfolder '{folder}' zostanie utworzony automatycznie", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Rozpocznij wyszukiwanie odcinków bez osiągniętego progu", + "AddNewSeriesSearchForMissingEpisodes": "Rozpocznij wyszukiwanie brakujących odcinków", "AddQualityProfile": "Dodaj profil jakości", - "AddQualityProfileError": "Nie udało się dodać nowego profilu jakości, spróbuj później.", - "AddReleaseProfile": "Dodaj Profil Wydania", - "AddRemotePathMapping": "Dodaj mapowanie ścieżek zdalnych", - "AgeWhenGrabbed": "Wiek (przy złapaniu)", + "AddQualityProfileError": "Nie udało się dodać nowego profilu jakości, spróbuj ponownie.", + "AddReleaseProfile": "Dodaj profil wydań", + "AddRemotePathMapping": "Dodaj mapowanie ścieżki zdalnej", + "AddRemotePathMappingError": "Nie można dodać nowego mapowania ścieżki zdalnej, spróbuj ponownie.", + "AddRootFolder": "Dodaj folder główny", + "AddRootFolderError": "Nie można dodać folderu głównego", + "AddSeriesWithTitle": "Dodaj {title}", + "AddToDownloadQueue": "Dodaj do kolejki pobierania", + "Added": "Dodano", + "AddedDate": "Dodano: {date}", + "AddedToDownloadQueue": "Dodano do kolejki pobierania", + "AddingTag": "Dodawanie tagu", + "AdvancedSettings": "Zaawansowane ustawienia", + "AfterManualRefresh": "Po ręcznym odświeżeniu", + "Age": "Wiek", + "AgeWhenGrabbed": "Wiek (w momencie pobrania)", + "Agenda": "Agenda", + "AirDate": "Data emisji", + "AirDateGracePeriod": "Okres tolerancji daty emisji", + "AirDateGracePeriodHelpText": "Wartości ujemne pozwalają pobierać przed datą emisji, wartości dodatnie uniemożliwiają pobieranie po dacie emisji.", + "AirDateRestriction": "Odrzucaj niewyemitowane wydania", + "AirDateRestrictionHelpText": "Uniemożliwia {appName} pobieranie wydań zawierających odcinki, które nie zostały jeszcze wyemitowane.", + "Airs": "Emisja", + "AirsDateAtTimeOn": "{date} o {time} na {networkLabel}", + "AirsTbaOn": "TBA na {networkLabel}", + "AirsTimeOn": "{time} na {networkLabel}", + "AirsTomorrowOn": "Jutro o {time} na {networkLabel}", + "All": "Wszystko", + "AllFiles": "Wszystkie pliki", + "AllResultsAreHiddenByTheAppliedFilter": "Wszystkie wyniki są ukryte przez zastosowany filtr", + "AllSeriesAreHiddenByTheAppliedFilter": "Wszystkie wyniki są ukryte przez zastosowany filtr", + "AllSeriesInRootFolderHaveBeenImported": "Wszystkie seriale w {path} zostały zaimportowane", "AllTitles": "Wszystkie tytuły", - "Any": "Dowolny", - "ApiKeyValidationHealthCheckMessage": "Zaktualizuj swój klucz API aby był długi na co najmniej {length} znaków. Możesz to zrobić poprzez ustawienia lub plik konfiguracyjny", + "AlreadyInYourLibrary": "Już w Twojej bibliotece", + "AlternateTitles": "Tytuły alternatywne", + "Always": "Zawsze", + "AnEpisodeIsDownloading": "Odcinek jest pobierany", + "AnalyseVideoFiles": "Analizuj pliki wideo", + "AnalyseVideoFilesHelpText": "Wyodrębnij informacje o wideo, takie jak rozdzielczość, czas trwania i kodeki. Wymaga to od {appName} odczytu fragmentów pliku, co może powodować wysoką aktywność dysku lub sieci podczas skanowania.", + "Analytics": "Analityka", + "AnalyticsEnabledHelpText": "Wysyłaj anonimowe dane o użyciu i błędach na serwery {appName}. Obejmuje to informacje o przeglądarce, używanych stronach WebUI {appName}, raportach błędów oraz wersji systemu i środowiska uruchomieniowego. Użyjemy tych informacji do priorytetyzacji funkcji i poprawek błędów.", + "Anime": "Anime", + "AnimeEpisodeFormat": "Format odcinka anime", + "AnimeEpisodeTypeDescription": "Odcinki wydane z użyciem bezwzględnego numeru odcinka", + "AnimeEpisodeTypeFormat": "Bezwzględny numer odcinka ({format})", + "Any": "Dowolne", + "ApiKey": "Klucz API", + "ApiKeyValidationHealthCheckMessage": "Zaktualizuj klucz API tak, aby miał co najmniej {length} znaków. Możesz to zrobić w ustawieniach lub pliku konfiguracyjnym", "AppDataDirectory": "Katalog AppData", - "AppUpdated": "{appName} Zaktualizowany", - "ApplicationUrlHelpText": "Zewnętrzny URL tej aplikacji zawierający http(s)://, port i adres URL", + "AppDataLocationHealthCheckMessage": "Aktualizacja nie będzie możliwa, aby zapobiec usunięciu AppData podczas aktualizacji", + "AppUpdated": "Zaktualizowano {appName}", + "AppUpdatedVersion": "{appName} został zaktualizowany do wersji `{version}`. Aby zobaczyć najnowsze zmiany, musisz ponownie wczytać {appName} ", + "ApplicationURL": "URL aplikacji", + "ApplicationUrlHelpText": "Zewnętrzny URL tej aplikacji, w tym http(s)://, port i bazowy URL", "Apply": "Zastosuj", "ApplyChanges": "Zastosuj zmiany", - "ApplyTagsHelpTextHowToApplyDownloadClients": "Jak", - "ApplyTagsHelpTextHowToApplyImportLists": "Jak zastosować tagi do wybranych list", - "ApplyTagsHelpTextHowToApplyIndexers": "Jak zastosować tagi do wybranych indeksatorów", - "ApplyTagsHelpTextRemove": "Usuń: usuń wprowadzone tagi", + "ApplyTags": "Zastosuj tagi", + "ApplyTagsHelpTextAdd": "Dodaj: Dodaj tagi do istniejącej listy tagów", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Jak zastosować tagi do wybranych klientów pobierania", + "ApplyTagsHelpTextHowToApplyImportLists": "Jak zastosować tagi do wybranych list importu", + "ApplyTagsHelpTextHowToApplyIndexers": "Jak zastosować tagi do wybranych indekserów", + "ApplyTagsHelpTextHowToApplySeries": "Jak zastosować tagi do wybranych seriali", + "ApplyTagsHelpTextRemove": "Usuń: Usuń wprowadzone tagi", + "ApplyTagsHelpTextReplace": "Zastąp: Zastąp tagi wprowadzonymi tagami (nie wpisuj tagów, aby wyczyścić wszystkie)", + "AptUpdater": "Użyj apt, aby zainstalować aktualizację", "AudioInfo": "Informacje o audio", - "AudioLanguages": "Języki Dźwięku", - "Authentication": "Autoryzacja", - "AuthenticationMethod": "Metoda Autoryzacji", - "AuthenticationMethodHelpTextWarning": "Wybierz prawidłową metodę autoryzacji", - "AuthenticationRequired": "Wymagana Autoryzacja", + "AudioLanguages": "Języki audio", + "AuthBasic": "Basic (okno przeglądarki)", + "AuthForm": "Formularze (strona logowania)", + "Authentication": "Uwierzytelnianie", + "AuthenticationMethod": "Metoda uwierzytelniania", + "AuthenticationMethodHelpText": "Wymagaj nazwy użytkownika i hasła, aby uzyskać dostęp do {appName}", + "AuthenticationMethodHelpTextWarning": "Wybierz prawidłową metodę uwierzytelniania", + "AuthenticationRequired": "Wymagane uwierzytelnianie", + "AuthenticationRequiredHelpText": "Zmień, które żądania wymagają uwierzytelniania. Nie zmieniaj, jeśli nie rozumiesz ryzyka.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potwierdź nowe hasło", + "AuthenticationRequiredPasswordHelpTextWarning": "Wprowadź nowe hasło", + "AuthenticationRequiredUsernameHelpTextWarning": "Wprowadź nową nazwę użytkownika", + "AuthenticationRequiredWarning": "Aby zapobiec zdalnemu dostępowi bez uwierzytelniania, {appName} wymaga teraz włączenia uwierzytelniania. Opcjonalnie możesz wyłączyć uwierzytelnianie dla adresów lokalnych.", + "AutoAdd": "Automatyczne dodawanie", + "AutoRedownloadFailed": "Automatyczne ponowne pobranie nieudane", + "AutoRedownloadFailedFromInteractiveSearch": "Automatyczne ponowne pobranie nieudane z wyszukiwania interaktywnego", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Automatycznie wyszukaj i spróbuj pobrać inne wydanie, gdy nieudane wydanie zostało pobrane z wyszukiwania interaktywnego", + "AutoRedownloadFailedHelpText": "Automatycznie wyszukaj i spróbuj pobrać inne wydanie", "AutoTagging": "Automatyczne tagowanie", + "AutoTaggingLoadError": "Nie można wczytać automatycznego tagowania", + "AutoTaggingNegateHelpText": "Jeśli zaznaczone, reguła automatycznego tagowania nie zostanie zastosowana, jeśli ten warunek {implementationName} pasuje.", + "AutoTaggingRequiredHelpText": "Ten warunek {implementationName} musi pasować, aby reguła automatycznego tagowania została zastosowana. W przeciwnym razie wystarczy pojedyncze dopasowanie {implementationName}.", + "AutoTaggingSpecificationGenre": "Gatunek(i)", + "AutoTaggingSpecificationMaximumYear": "Maksymalny rok", + "AutoTaggingSpecificationMinimumYear": "Minimalny rok", + "AutoTaggingSpecificationNetwork": "Sieć(ci)", + "AutoTaggingSpecificationOriginalCountry": "Kraj", + "AutoTaggingSpecificationOriginalLanguage": "Język", + "AutoTaggingSpecificationQualityProfile": "Profil jakości", + "AutoTaggingSpecificationRootFolder": "Folder główny", + "AutoTaggingSpecificationSeriesType": "Typ serialu", + "AutoTaggingSpecificationStatus": "Status", + "AutoTaggingSpecificationTag": "Tag", + "Automatic": "Automatycznie", + "AutomaticAdd": "Automatyczne dodawanie", + "AutomaticSearch": "Wyszukiwanie automatyczne", + "AutomaticUpdatesDisabledDocker": "Automatyczne aktualizacje nie są bezpośrednio obsługiwane przy użyciu mechanizmu aktualizacji Docker. Musisz zaktualizować obraz kontenera poza {appName} lub użyć skryptu", + "AverageSize": "Średni rozmiar", + "AverageSizePerEpisode": "Średni rozmiar na odcinek", + "Backup": "Kopia zapasowa", + "BackupFolderHelpText": "Ścieżki względne będą w katalogu AppData aplikacji {appName}", + "BackupIntervalHelpText": "Interwał między automatycznymi kopiami zapasowymi", + "BackupNow": "Utwórz kopię teraz", + "BackupRetentionHelpText": "Automatyczne kopie zapasowe starsze niż okres przechowywania będą automatycznie czyszczone", + "Backups": "Kopie zapasowe", + "BackupsLoadError": "Nie można wczytać kopii zapasowych", + "BeforeUpdate": "Przed aktualizacją", + "BindAddress": "Adres nasłuchu", "BindAddressHelpText": "Prawidłowy adres IP, localhost lub '*' dla wszystkich interfejsów", - "BlocklistRelease": "Dodaj wersję do czarnej listy", - "BlocklistReleases": "Dodaj wersje do czarnej listy", + "BlackholeFolderHelpText": "Folder, w którym {appName} zapisze plik {extension}", + "BlackholeWatchFolder": "Folder obserwowany", + "BlackholeWatchFolderHelpText": "Folder, z którego {appName} powinien importować ukończone pobrania", + "Blocklist": "Czarna lista", + "BlocklistAndSearch": "Dodaj do czarnej listy i wyszukaj", + "BlocklistAndSearchHint": "Rozpocznij wyszukiwanie zamiennika po dodaniu do czarnej listy", + "BlocklistAndSearchMultipleHint": "Rozpocznij wyszukiwania zamienników po dodaniu do czarnej listy", + "BlocklistFilterHasNoItems": "Wybrany filtr czarnej listy nie zawiera elementów", + "BlocklistLoadError": "Nie można wczytać czarnej listy", + "BlocklistMultipleOnlyHint": "Dodaj do czarnej listy bez wyszukiwania zamienników", + "BlocklistOnly": "Tylko czarna lista", + "BlocklistOnlyHint": "Czarna lista bez wyszukiwania zamiennika", + "BlocklistRelease": "Dodaj wydanie do czarnej listy", + "BlocklistReleaseHelpText": "Blokuje ponowne pobranie tego wydania przez {appName} przez RSS lub wyszukiwanie automatyczne", + "BlocklistReleases": "Wydania na czarnej liście", + "Blocklisted": "Na czarnej liście", + "BlocklistedAt": "Dodano do czarnej listy: {date}", + "Branch": "Gałąź", + "BranchUpdate": "Gałąź używana do aktualizacji {appName}", + "BranchUpdateMechanism": "Gałąź używana przez zewnętrzny mechanizm aktualizacji", + "BrowserReloadRequired": "Wymagane ponowne wczytanie przeglądarki", + "BuiltIn": "Wbudowane", + "BypassDelayIfAboveCustomFormatScore": "Pomiń opóźnienie, jeśli wynik formatu niestandardowego jest wyższy", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Włącz pomijanie opóźnienia, gdy wydanie ma wynik wyższy niż skonfigurowane minimum formatu niestandardowego", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimalny wynik formatu niestandardowego", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Minimalny wynik formatu niestandardowego wymagany do pominięcia opóźnienia dla preferowanego protokołu", + "BypassDelayIfHighestQuality": "Pomiń opóźnienie przy najwyższej jakości", + "BypassDelayIfHighestQualityHelpText": "Pomiń opóźnienie, gdy wydanie ma najwyższą włączoną jakość w profilu jakości z preferowanym protokołem", + "BypassProxyForLocalAddresses": "Pomiń proxy dla adresów lokalnych", + "Calendar": "Kalendarz", + "CalendarFeed": "Kanał kalendarza {appName}", + "CalendarLegendEpisodeDownloadedTooltip": "Odcinek został pobrany i posortowany", + "CalendarLegendEpisodeDownloadingTooltip": "Odcinek jest obecnie pobierany", + "CalendarLegendEpisodeMissingTooltip": "Odcinek został wyemitowany i brakuje go na dysku", + "CalendarLegendEpisodeOnAirTooltip": "Odcinek jest obecnie emitowany", + "CalendarLegendEpisodeUnairedTooltip": "Odcinek nie został jeszcze wyemitowany", + "CalendarLegendEpisodeUnmonitoredTooltip": "Odcinek nie jest monitorowany", + "CalendarLegendSeriesFinaleTooltip": "Finał serialu lub sezonu", + "CalendarLegendSeriesPremiereTooltip": "Premiera serialu lub sezonu", + "CalendarLoadError": "Nie można wczytać kalendarza", "CalendarOptions": "Opcje kalendarza", + "Cancel": "Anuluj", + "CancelPendingTask": "Czy na pewno chcesz anulować to oczekujące zadanie?", + "CancelProcessing": "Anuluj przetwarzanie", + "Category": "Kategoria", + "CertificateValidation": "Weryfikacja certyfikatu", + "CertificateValidationHelpText": "Zmień poziom rygoru walidacji certyfikatu HTTPS. Nie zmieniaj, jeśli nie rozumiesz ryzyka.", + "Certification": "Certyfikacja", + "ChangeCategory": "Zmień kategorię", + "ChangeCategoryHint": "Zmienia pobranie na kategorię 'Post-Import Category' klienta pobierania", + "ChangeCategoryMultipleHint": "Zmienia pobrania na kategorię 'Post-Import Category' klienta pobierania", + "ChangeFileDate": "Zmień datę pliku", + "ChangeFileDateHelpText": "Zmień datę pliku podczas importu/przeskanowania", + "CheckDownloadClientForDetails": "sprawdź klienta pobierania, aby uzyskać więcej szczegółów", + "ChmodFolder": "Folder chmod", + "ChmodFolderHelpText": "Wartość ósemkowa, stosowana podczas importu/zmiany nazwy do folderów i plików multimediów (bez bitów wykonywania)", + "ChmodFolderHelpTextWarning": "To działa tylko, jeśli użytkownik uruchamiający {appName} jest właścicielem pliku. Lepiej upewnić się, że klient pobierania ustawia uprawnienia prawidłowo.", + "ChooseAnotherFolder": "Wybierz inny folder", + "ChooseImportMode": "Wybierz tryb importu", + "ChownGroup": "Grupa chown", + "ChownGroupHelpText": "Nazwa grupy lub gid. Użyj gid dla zdalnych systemów plików.", + "ChownGroupHelpTextWarning": "To działa tylko, jeśli użytkownik uruchamiający {appName} jest właścicielem pliku. Lepiej upewnić się, że klient pobierania używa tej samej grupy co {appName}.", + "CleanLibraryLevel": "Poziom czyszczenia biblioteki", + "Clear": "Wyczyść", + "ClearBlocklist": "Wyczyść czarną listę", + "ClearBlocklistMessageText": "Czy na pewno chcesz usunąć wszystkie elementy z czarnej listy?", + "ClickToChangeEpisode": "Kliknij, aby zmienić odcinek", + "ClickToChangeIndexerFlags": "Kliknij, aby zmienić flagi indeksera", + "ClickToChangeLanguage": "Kliknij, aby zmienić język", + "ClickToChangeQuality": "Kliknij, aby zmienić jakość", + "ClickToChangeReleaseGroup": "Kliknij, aby zmienić grupę wydania", + "ClickToChangeReleaseType": "Kliknij, aby zmienić typ wydania", + "ClickToChangeSeason": "Kliknij, aby zmienić sezon", + "ClickToChangeSeries": "Kliknij, aby zmienić serial", + "ClientPriority": "Priorytet klienta", + "Clone": "Klonuj", + "CloneAutoTag": "Klonuj automatyczny tag", + "CloneCondition": "Klonuj warunek", + "CloneCustomFormat": "Klonuj format niestandardowy", + "CloneImportList": "Klonuj listę importu", + "CloneIndexer": "Klonuj indekser", + "CloneProfile": "Klonuj profil", "Close": "Zamknij", + "CollapseAll": "Zwiń wszystko", + "CollapseMultipleEpisodes": "Zwiń wiele odcinków", + "CollapseMultipleEpisodesHelpText": "Zwiń wiele odcinków emitowanych tego samego dnia", + "CollectionsLoadError": "Nie można wczytać kolekcji", + "ColonReplacement": "Zastępowanie dwukropka", + "ColonReplacementFormatHelpText": "Zmień sposób, w jaki {appName} obsługuje zastępowanie dwukropka", + "Completed": "Ukończono", + "CompletedDownloadHandling": "Obsługa ukończonych pobrań", + "Component": "Komponent", + "Condition": "Warunek", + "ConditionUsingRegularExpressions": "Ten warunek dopasowuje przy użyciu wyrażeń regularnych. Zwróć uwagę, że znaki `\\^$.|?*+()[{` mają specjalne znaczenie i wymagają escapowania przez `\\`", + "Conditions": "Warunki", "Connect": "Połączenia", - "CouldNotFindResults": "Nie można znaleźć żadnych wyników dla „{term}”", - "CustomFormatUnknownCondition": "Nieznany warunek formatu niestandardowego „{implementation}\"", - "CustomFormatUnknownConditionOption": "Nieznana opcja „{key}” dla warunku „{implementation}”", - "CutoffUnmet": "Odcięcie niespełnione", - "Dash": "Dash", - "DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową „{name}”?", - "DeleteConditionMessageText": "Czy na pewno chcesz usunąć tag „{name}”?", - "DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania „{name}”?", - "DeleteIndexerMessageText": "Czy na pewno chcesz usunąć indeksator „{name}”?", - "DeleteReleaseProfileMessageText": "Czy na pewno usunąć informacje dodatkowe '{name}'?", - "DownloadClientDownloadStationValidationApiVersion": "Pobrana wersja Station API nie jest wspierana, minimalna wspierana wersja to {requiredVersion}. Powinna być między {minVersion} a {maxVersion}", + "ConnectSettings": "Ustawienia połączeń", + "ConnectSettingsSummary": "Powiadomienia, połączenia z serwerami/odtwarzaczami multimediów i niestandardowe skrypty", + "Connection": "Połączenie", + "ConnectionLost": "Utracono połączenie", + "ConnectionLostReconnect": "{appName} spróbuje połączyć się automatycznie lub możesz kliknąć przeładuj poniżej.", + "ConnectionLostToBackend": "{appName} utracił połączenie z backendem i musi zostać ponownie wczytany, aby przywrócić funkcjonalność.", + "ConnectionSettingsUrlBaseHelpText": "Dodaje prefiks do adresu URL {connectionName}, np. {url}", + "Connections": "Połączenia", + "ConnectionsLoadError": "Nie można wczytać połączeń", + "Continuing": "Kontynuowany", + "ContinuingOnly": "Tylko kontynuowane", + "ContinuingSeriesDescription": "Oczekiwane są kolejne odcinki/kolejny sezon", + "CopyToClipboard": "Kopiuj do schowka", + "CopyUsingHardlinksHelpTextWarning": "Czasami blokady plików mogą uniemożliwić zmianę nazwy plików, które są seedowane. Możesz tymczasowo wyłączyć seedowanie i użyć funkcji zmiany nazwy {appName} jako obejścia.", + "CopyUsingHardlinksSeriesHelpText": "Dowiązania twarde pozwalają {appName} importować seedowane torrenty do folderu serialu bez zajmowania dodatkowego miejsca na dysku i bez kopiowania całej zawartości pliku. Dowiązania twarde zadziałają tylko wtedy, gdy źródło i cel są na tym samym woluminie", + "CouldNotFindResults": "Nie znaleziono wyników dla '{term}'", + "CountCustomFormatsSelected": "Wybrano {count} format(ów) niestandardowych", + "CountDownloadClientsSelected": "Wybrano {count} klient(ów) pobierania", + "CountImportListsSelected": "Wybrano {count} list(y) importu", + "CountIndexersSelected": "Wybrano {count} indekser(y)", + "CountSeasons": "{count} sezonów", + "CountSelectedFile": "Wybrano {selectedCount} plik", + "CountSelectedFiles": "Wybrano {selectedCount} pliki", + "CountSeriesSelected": "Wybrano {count} seriali", + "CountVotes": "{votes} głosów", + "CreateEmptySeriesFolders": "Twórz puste foldery seriali", + "CreateEmptySeriesFoldersHelpText": "Twórz brakujące foldery seriali podczas skanowania dysku", + "CreateGroup": "Utwórz grupę", + "CurrentlyInstalled": "Obecnie zainstalowane", + "Custom": "Niestandardowe", + "CustomColonReplacement": "Niestandardowe zastępowanie dwukropka", + "CustomColonReplacementFormatHelpText": "Znaki używane do zastąpienia dwukropków", + "CustomColonReplacementFormatHint": "Prawidłowy znak systemu plików, np. dwukropek (litera)", + "CustomFilter": "Filtr niestandardowy", + "CustomFilters": "Filtry niestandardowe", + "CustomFormat": "Format niestandardowy", + "CustomFormatHelpText": "{appName} ocenia każde wydanie na podstawie sumy punktów za pasujące formaty niestandardowe. Jeśli nowe wydanie poprawia wynik przy tej samej lub lepszej jakości, {appName} je pobierze.", + "CustomFormatJson": "JSON formatu niestandardowego", + "CustomFormatScore": "Wynik formatu niestandardowego", + "CustomFormatUnknownCondition": "Nieznany warunek formatu niestandardowego '{implementation}'", + "CustomFormatUnknownConditionOption": "Nieznana opcja '{key}' dla warunku '{implementation}'", + "CustomFormats": "Formaty niestandardowe", + "CustomFormatsLoadError": "Nie można wczytać formatów niestandardowych", + "CustomFormatsSettings": "Ustawienia formatów niestandardowych", + "CustomFormatsSettingsSummary": "Formaty niestandardowe i ustawienia", + "CustomFormatsSettingsTriggerInfo": "Format niestandardowy zostanie zastosowany do wydania lub pliku, gdy dopasuje co najmniej jeden warunek z każdego wybranego typu warunku.", + "CustomFormatsSpecificationExceptLanguage": "Poza językiem", + "CustomFormatsSpecificationExceptLanguageHelpText": "Dopasowuje, jeśli obecny jest jakikolwiek język inny niż wybrany", + "CustomFormatsSpecificationFlag": "Flaga", + "CustomFormatsSpecificationLanguage": "Język", + "CustomFormatsSpecificationMaximumSize": "Maksymalny rozmiar", + "CustomFormatsSpecificationMaximumSizeHelpText": "Wydanie musi być mniejsze lub równe temu rozmiarowi", + "CustomFormatsSpecificationMinimumSize": "Minimalny rozmiar", + "CustomFormatsSpecificationMinimumSizeHelpText": "Wydanie musi być większe niż ten rozmiar", + "CustomFormatsSpecificationRegularExpression": "Wyrażenie regularne", + "CustomFormatsSpecificationRegularExpressionHelpText": "RegEx formatu niestandardowego nie rozróżnia wielkości liter", + "CustomFormatsSpecificationReleaseGroup": "Grupa wydania", + "CustomFormatsSpecificationResolution": "Rozdzielczość", + "CustomFormatsSpecificationSource": "Źródło", + "Cutoff": "Próg", + "CutoffNotMet": "Próg nieosiągnięty", + "CutoffUnmet": "Nieosiągnięty próg", + "CutoffUnmetLoadError": "Błąd ładowania elementów z nieosiągniętym progiem", + "CutoffUnmetNoItems": "Brak elementów z nieosiągniętym progiem", + "Daily": "Dzienny", + "DailyEpisodeFormat": "Dzienny format odcinka", + "DailyEpisodeTypeDescription": "Odcinki wydawane codziennie lub rzadziej, używające formatu rok-miesiąc-dzień (2023-08-04)", + "DailyEpisodeTypeFormat": "Data ({format})", + "Dash": "Myślnik", + "Database": "Baza danych", + "DatabaseMigration": "Migracja bazy danych", + "Date": "Data", + "Dates": "Daty", + "Day": "Dzień", + "DayOfWeekAt": "{day} o {time}", + "Debug": "Debugowanie", + "Default": "Domyślne", + "DefaultCase": "Domyślny przypadek", + "DefaultDelayProfileSeries": "To jest profil domyślny. Ma zastosowanie do wszystkich seriali, które nie mają przypisanego jawnego profilu.", + "DefaultNameCopiedImportList": "{name} - Kopia", + "DefaultNameCopiedProfile": "{name} - Kopia", + "DefaultNameCopiedSpecification": "{name} - Kopia", + "DefaultNotFoundMessage": "Chyba się zgubiłeś, nie ma tu nic do zobaczenia.", + "Delay": "Opóźnienie", + "DelayMinutes": "{delay} minut", + "DelayProfile": "Profil opóźnienia", + "DelayProfileProtocol": "Protokół: {preferredProtocol}", + "DelayProfileSeriesTagsHelpText": "Dotyczy seriali z co najmniej jednym pasującym tagiem", + "DelayProfiles": "Profile opóźnienia", + "DelayProfilesLoadError": "Nie można wczytać profili opóźnienia", + "DelayingDownloadUntil": "Opóźnianie pobierania do {date} o {time}", + "Delete": "Usuń", + "DeleteAutoTag": "Usuń automatyczny tag", + "DeleteAutoTagHelpText": "Czy na pewno chcesz usunąć automatyczny tag '{name}'?", + "DeleteBackup": "Usuń kopię zapasową", + "DeleteBackupMessageText": "Czy na pewno chcesz usunąć kopię zapasową '{name}'?", + "DeleteCondition": "Usuń warunek", + "DeleteConditionMessageText": "Czy na pewno chcesz usunąć warunek '{name}'?", + "DeleteCustomFormat": "Usuń format niestandardowy", + "DeleteCustomFormatMessageText": "Czy na pewno chcesz usunąć format niestandardowy '{name}'?", + "DeleteDelayProfile": "Usuń profil opóźnienia", + "DeleteDelayProfileMessageText": "Czy na pewno chcesz usunąć ten profil opóźnienia?", + "DeleteDownloadClient": "Usuń klienta pobierania", + "DeleteDownloadClientMessageText": "Czy na pewno chcesz usunąć klienta pobierania '{name}'?", + "DeleteEmptyFolders": "Usuń puste foldery", + "DeleteEmptySeriesFoldersHelpText": "Usuwaj puste foldery seriali i sezonów podczas skanowania dysku oraz gdy pliki odcinków są usuwane", + "DeleteEpisodeFile": "Usuń plik odcinka", + "DeleteEpisodeFileMessage": "Czy na pewno chcesz usunąć '{path}'?", + "DeleteEpisodeFromDisk": "Usuń odcinek z dysku", + "DeleteEpisodesFiles": "Usuń {episodeFileCount} plików odcinków", + "DeleteEpisodesFilesHelpText": "Usuń pliki odcinków i folder serialu", + "DeleteFiles": "Usuń pliki", + "DeleteImportList": "Usuń listę importu", + "DeleteImportListExclusion": "Usuń wykluczenie listy importu", + "DeleteImportListExclusionMessageText": "Czy na pewno chcesz usunąć to wykluczenie listy importu?", + "DeleteImportListMessageText": "Czy na pewno chcesz usunąć listę '{name}'?", + "DeleteIndexer": "Usuń indekser", + "DeleteIndexerMessageText": "Czy na pewno chcesz usunąć indekser '{name}'?", + "DeleteNotification": "Usuń powiadomienie", + "DeleteNotificationMessageText": "Czy na pewno chcesz usunąć powiadomienie '{name}'?", + "DeleteQualityProfile": "Usuń profil jakości", + "DeleteQualityProfileMessageText": "Czy na pewno chcesz usunąć profil jakości '{name}'?", + "DeleteReleaseProfile": "Usuń profil wydań", + "DeleteReleaseProfileMessageText": "Czy na pewno chcesz usunąć profil wydań '{name}'?", + "DeleteRemotePathMapping": "Usuń mapowanie ścieżki zdalnej", + "DeleteRemotePathMappingMessageText": "Czy na pewno chcesz usunąć to mapowanie ścieżki zdalnej?", + "DeleteSelected": "Usuń zaznaczone", + "DeleteSelectedCustomFormats": "Usuń format(y) niestandardowe", + "DeleteSelectedCustomFormatsMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych formatów niestandardowych?", + "DeleteSelectedDownloadClients": "Usuń klienta(ów) pobierania", + "DeleteSelectedDownloadClientsMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych klientów pobierania?", + "DeleteSelectedEpisodeFiles": "Usuń zaznaczone pliki odcinków", + "DeleteSelectedEpisodeFilesHelpText": "Czy na pewno chcesz usunąć zaznaczone pliki odcinków?", + "DeleteSelectedImportListExclusionsMessageText": "Czy na pewno chcesz usunąć zaznaczone wykluczenia list importu?", + "DeleteSelectedImportLists": "Usuń listę/listy importu", + "DeleteSelectedImportListsMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych list importu?", + "DeleteSelectedIndexers": "Usuń indekser(y)", + "DeleteSelectedIndexersMessageText": "Czy na pewno chcesz usunąć {count} zaznaczonych indekserów?", + "DeleteSelectedSeries": "Usuń zaznaczone seriale", + "DeleteSelectedSeriesFiles": "Usuń pliki zaznaczonych seriali", + "DeleteSeriesFilesConfirmation": "Czy na pewno chcesz usunąć wszystkie śledzone pliki odcinków dla {count} zaznaczonych seriali?", + "DeleteSeriesFolder": "Usuń folder serialu", + "DeleteSeriesFolderConfirmation": "Folder serialu `{path}` oraz cała jego zawartość zostaną usunięte.", + "DeleteSeriesFolderCountConfirmation": "Czy na pewno chcesz usunąć {count} zaznaczonych seriali?", + "DeleteSeriesFolderCountWithFilesConfirmation": "Czy na pewno chcesz usunąć {count} zaznaczonych seriali i całą ich zawartość?", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} plików odcinków o łącznym rozmiarze {size}", + "DeleteSeriesFolderHelpText": "Usuń folder serialu i jego zawartość", + "DeleteSeriesFolders": "Usuń foldery seriali", + "DeleteSeriesFoldersHelpText": "Usuń foldery seriali i całą ich zawartość", + "DeleteSeriesModalHeader": "Usuń - {title}", + "DeleteSpecification": "Usuń specyfikację", + "DeleteSpecificationHelpText": "Czy na pewno chcesz usunąć specyfikację '{name}'?", + "DeleteTag": "Usuń tag", + "DeleteTagMessageText": "Czy na pewno chcesz usunąć tag '{label}'?", + "Deleted": "Usunięto", + "DeletedReasonEpisodeMissingFromDisk": "{appName} nie mógł znaleźć pliku na dysku, więc plik został odłączony od odcinka w bazie danych", + "DeletedReasonManual": "Plik został usunięty przez {appName}, ręcznie lub przez inne narzędzie przez API", + "DeletedReasonUpgrade": "Plik został usunięty w celu zaimportowania ulepszonej wersji", + "DeletedSeriesDescription": "Serial został usunięty z TheTVDB", + "Destination": "Miejsce docelowe", + "DestinationPath": "Ścieżka docelowa", + "DestinationRelativePath": "Względna ścieżka docelowa", + "DetailedProgressBar": "Szczegółowy pasek postępu", + "DetailedProgressBarHelpText": "Pokazuj tekst na pasku postępu", + "Details": "Szczegóły", + "Directory": "Katalog", + "Disabled": "Wyłączone", + "DisabledForLocalAddresses": "Wyłączone dla adresów lokalnych", + "Discord": "Discord", + "DiskSpace": "Miejsce na dysku", + "DoNotBlocklist": "Nie dodawaj do czarnej listy", + "DoNotBlocklistHint": "Usuń bez dodawania do czarnej listy", + "DoNotPrefer": "Nie preferuj", + "DoNotUpgradeAutomatically": "Nie aktualizuj automatycznie", + "Docker": "Docker", + "DockerUpdater": "Zaktualizuj kontener Docker, aby otrzymać aktualizację", + "Donate": "Wesprzyj", + "Donations": "Darowizny", + "DoneEditingGroups": "Zakończono edycję grup", + "DoneEditingSizes": "Zakończono edycję rozmiarów", + "DotNetVersion": ".NET", + "Download": "Pobierz", + "DownloadClient": "Klient pobierania", + "DownloadClientAriaSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Aria2", + "DownloadClientCheckNoneAvailableHealthCheckMessage": "Brak dostępnego klienta pobierania", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nie można komunikować się z {downloadClientName}. {errorMessage}", + "DownloadClientDelugeSettingsDirectory": "Katalog pobierania", + "DownloadClientDelugeSettingsDirectoryCompleted": "Katalog przenoszenia po ukończeniu", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Opcjonalna lokalizacja przenoszenia ukończonych pobrań, pozostaw puste aby użyć domyślnej lokalizacji Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Deluge", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Dodaje prefiks do URL JSON Deluge, zobacz {url}", + "DownloadClientDelugeTorrentStateError": "Deluge zgłasza błąd", + "DownloadClientDelugeValidationLabelPluginFailure": "Konfiguracja etykiety nie powiodła się", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} nie mógł dodać etykiety do {clientName}.", + "DownloadClientDelugeValidationLabelPluginInactive": "Wtyczka Label nie jest aktywna", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Aby używać kategorii, musisz mieć włączoną wtyczkę Label w {clientName}.", + "DownloadClientDownloadStationProviderMessage": "{appName} nie może połączyć się z Download Station, jeśli na Twoim koncie DSM jest włączone uwierzytelnianie dwuskładnikowe", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Opcjonalny folder współdzielony, do którego trafiają pobrania; pozostaw puste, aby użyć domyślnej lokalizacji Download Station", + "DownloadClientDownloadStationValidationApiVersion": "Wersja API Download Station nie jest obsługiwana, powinna wynosić co najmniej {requiredVersion}. Obsługiwany zakres: od {minVersion} do {maxVersion}", "DownloadClientDownloadStationValidationFolderMissing": "Folder nie istnieje", - "DownloadClientDownloadStationValidationFolderMissingDetail": "Folder \"{downloadDir}\" nie istnieje. Musi on zostać utworzony manualnie wewnątrz folderu współdzielonego \"{sharedFolder}\".", - "DownloadClientDownloadStationValidationNoDefaultDestination": "Nie zdefiniowano domyślne lokalizacji", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Musisz zalogować się do swojej stacji DiskStation jako {username} i ręcznie skonfigurować to w ustawieniach DownloadStation w sekcji BT/HTTP/FTP/NZB -> Lokalizacja.", + "DownloadClientDownloadStationValidationFolderMissingDetail": "Folder '{downloadDir}' nie istnieje, musi zostać utworzony ręcznie wewnątrz folderu współdzielonego '{sharedFolder}'.", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Brak domyślnego miejsca docelowego", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Zaloguj się do Diskstation jako {username} i ustaw to ręcznie w ustawieniach DownloadStation: BT/HTTP/FTP/NZB -> Location.", "DownloadClientDownloadStationValidationSharedFolderMissing": "Folder współdzielony nie istnieje", - "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Stacja DiskStation nie posiada folderu współdzielonego o nazwie „{sharedFolder}”. Czy na pewno podana nazwa jest poprawna?", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation nie ma folderu współdzielonego o nazwie '{sharedFolder}'. Czy na pewno podano poprawną nazwę?", + "DownloadClientFloodSettingsAdditionalTags": "Dodatkowe tagi", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Dodaje właściwości mediów jako tagi. Podpowiedzi są przykładami.", "DownloadClientFloodSettingsPostImportTags": "Tagi po imporcie", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Dodaje tagi po zaimportowaniu pobrania. Torrenty ze wszystkimi tymi tagami będą filtrowane z kolejki i nie będą usuwane automatycznie.", + "DownloadClientFloodSettingsRemovalInfo": "{appName} będzie automatycznie usuwać torrenty na podstawie bieżących kryteriów seedowania w Ustawienia -> Indeksery", + "DownloadClientFloodSettingsStartOnAdd": "Uruchom przy dodaniu", + "DownloadClientFloodSettingsTagsHelpText": "Początkowe tagi pobrania. Aby zostało rozpoznane, pobranie musi mieć wszystkie tagi początkowe. Pozwala to uniknąć konfliktów z niepowiązanymi pobraniami.", + "DownloadClientFloodSettingsUrlBaseHelpText": "Dodaje prefiks do API Flood, np. {url}", + "DownloadClientFreeboxApiError": "API Freebox zwróciło błąd: {errorDescription}", + "DownloadClientFreeboxAuthenticationError": "Uwierzytelnianie w API Freebox nie powiodło się. Powód: {errorDescription}", + "DownloadClientFreeboxNotLoggedIn": "Niezalogowano", + "DownloadClientFreeboxSettingsApiUrl": "URL API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Zdefiniuj bazowy URL API Freebox wraz z wersją API, np. '{url}', domyślnie '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "ID aplikacji", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID aplikacji podane podczas tworzenia dostępu do API Freebox (tj. 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "Token aplikacji", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikacji uzyskany podczas tworzenia dostępu do API Freebox (tj. 'app_token')", + "DownloadClientFreeboxSettingsHostHelpText": "Nazwa hosta lub adres IP hosta Freebox, domyślnie '{url}' (zadziała tylko w tej samej sieci)", + "DownloadClientFreeboxSettingsPortHelpText": "Port używany do dostępu do interfejsu Freebox, domyślnie '{port}'", + "DownloadClientFreeboxUnableToReachFreebox": "Nie można połączyć się z API Freebox. Zweryfikuj ustawienia 'Host', 'Port' lub 'Use SSL'. (Błąd: {exceptionMessage})", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Nie można połączyć się z API Freebox. Zweryfikuj ustawienie 'API URL' dla bazowego URL i wersji.", + "DownloadClientItemErrorMessage": "{clientName} zgłasza błąd: {message}", + "DownloadClientNzbVortexMultipleFilesMessage": "Pobranie zawiera wiele plików i nie znajduje się w folderze zadania: {outputPath}", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Ta opcja wymaga co najmniej NzbGet w wersji 16.0", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "Ustawienie KeepHistory w NzbGet powinno być mniejsze niż 25000", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Ustawienie KeepHistory w NzbGet jest ustawione zbyt wysoko.", + "DownloadClientNzbgetValidationKeepHistoryZero": "Ustawienie KeepHistory w NzbGet powinno być większe niż 0", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Ustawienie KeepHistory w NzbGet jest ustawione na 0, co uniemożliwia {appName} wykrywanie ukończonych pobrań.", + "DownloadClientOptionsLoadError": "Nie można wczytać opcji klienta pobierania", + "DownloadClientPneumaticSettingsNzbFolder": "Folder NZB", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Ten folder musi być osiągalny z XBMC", + "DownloadClientPneumaticSettingsStrmFolder": "Folder Strm", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Pliki .strm w tym folderze będą importowane przez drone", + "DownloadClientPriorityHelpText": "Priorytet klienta pobierania od 1 (najwyższy) do 50 (najniższy). Domyślnie: 1. Dla klientów z tym samym priorytetem używany jest Round-Robin.", + "DownloadClientQbittorrentSettingsAddSeriesTags": "Dodaj tagi serialu", + "DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Dodawaj tagi serialu do nowych torrentów dodawanych do klienta pobierania (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsContentLayout": "Układ zawartości", + "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Czy używać układu zawartości skonfigurowanego w qBittorrent, oryginalnego układu z torrenta, czy zawsze tworzyć podfolder (qBittorrent 4.3.2+)", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Najpierw pierwsze i ostatnie", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Pobieraj najpierw pierwsze i ostatnie fragmenty (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Stan początkowy dla torrentów dodawanych do qBittorrent. Pamiętaj, że wymuszone torrenty nie przestrzegają ograniczeń seedowania", + "DownloadClientQbittorrentSettingsSequentialOrder": "Kolejność sekwencyjna", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Pobieraj w kolejności sekwencyjnej (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Użyj bezpiecznego połączenia. Zobacz Opcje -> Web UI -> 'Use HTTPS instead of HTTP' w qBittorrent.", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent nie może rozwiązać linku magnet przy wyłączonym DHT", + "DownloadClientQbittorrentTorrentStateError": "qBittorrent zgłasza błąd", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent pobiera metadane", + "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent zgłasza brakujące pliki", + "DownloadClientQbittorrentTorrentStatePathError": "Nie można zaimportować. Ścieżka odpowiada bazowemu katalogowi pobrań klienta, możliwe że dla tego torrenta wyłączono 'Keep top-level folder' albo 'Torrent Content Layout' NIE jest ustawione na 'Original' lub 'Create Subfolder'?", + "DownloadClientQbittorrentTorrentStateStalled": "Pobieranie utknęło bez połączeń", + "DownloadClientQbittorrentTorrentStateUnknown": "Nieznany stan pobierania: {state}", + "DownloadClientQbittorrentValidationCategoryAddFailure": "Konfiguracja kategorii nie powiodła się", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} nie mógł dodać etykiety do qBittorrent.", + "DownloadClientQbittorrentValidationCategoryRecommended": "Kategoria jest zalecana", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} nie będzie próbował importować ukończonych pobrań bez kategorii.", + "DownloadClientQbittorrentValidationCategoryUnsupported": "Kategoria nie jest obsługiwana", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Kategorie nie są obsługiwane do wersji qBittorrent 3.3.0. Zaktualizuj lub spróbuj ponownie z pustą kategorią.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Kolejkowanie nie jest włączone", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Kolejkowanie torrentów nie jest włączone w ustawieniach qBittorrent. Włącz je w qBittorrent lub wybierz priorytet 'Ostatni'.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent jest skonfigurowany do usuwania torrentów po osiągnięciu limitu współczynnika udostępniania", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} nie będzie mógł wykonać skonfigurowanej obsługi ukończonych pobrań. Możesz to naprawić w qBittorrent ('Narzędzia -> Opcje...' w menu), zmieniając 'Opcje -> BitTorrent -> Ograniczenie współczynnika udostępniania' z 'Usuń je' na 'Wstrzymaj je'", + "DownloadClientRTorrentProviderMessage": "rTorrent nie wstrzyma torrentów po spełnieniu kryteriów seedowania. {appName} będzie automatycznie usuwał torrenty na podstawie bieżących kryteriów seedowania w Ustawienia->Indeksery tylko wtedy, gdy włączone jest Usuwanie ukończonych. Po imporcie ustawi też {importedView} jako widok rTorrent, który można wykorzystać w skryptach rTorrent do dostosowania zachowania.", + "DownloadClientRTorrentSettingsAddStopped": "Dodaj zatrzymane", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Włączenie spowoduje dodawanie torrentów i magnetów do rTorrent w stanie zatrzymanym. To może psuć pliki magnet.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji rTorrent", + "DownloadClientRTorrentSettingsUrlPath": "Ścieżka URL", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Ścieżka do endpointu XMLRPC, zobacz {url}. Zwykle jest to RPC2 lub [ścieżka do ruTorrent]{url2} przy użyciu ruTorrent.", + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient pobierania {downloadClientName} jest ustawiony na usuwanie ukończonych pobrań. Może to powodować usunięcie pobrań z klienta zanim {appName} zdąży je zaimportować.", + "DownloadClientRootFolderHealthCheckMessage": "Klient pobierania {downloadClientName} zapisuje pobrania w folderze głównym {rootFolderPath}. Nie powinieneś pobierać do folderu głównego.", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Wyłącz opcję 'Sprawdzaj przed pobraniem' w Sabnzbd", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Używanie opcji 'Sprawdzaj przed pobraniem' wpływa na zdolność {appName} do śledzenia nowych pobrań. Dodatkowo Sabnzbd zaleca zamiast tego opcję 'Przerywaj zadania, których nie można ukończyć', ponieważ jest skuteczniejsza.", + "DownloadClientSabnzbdValidationDevelopVersion": "Wersja deweloperska Sabnzbd, zakłada się wersję 3.0.0 lub nowszą.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} może nie być w stanie obsłużyć nowych funkcji dodanych do SABnzbd podczas używania wersji deweloperskich.", + "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Wyłącz sortowanie po dacie", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Musisz wyłączyć sortowanie po dacie dla kategorii używanej przez {appName}, aby zapobiec problemom z importem. Przejdź do Sabnzbd, aby to naprawić.", + "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Wyłącz sortowanie filmów", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Musisz wyłączyć sortowanie filmów dla kategorii używanej przez {appName}, aby zapobiec problemom z importem. Przejdź do Sabnzbd, aby to naprawić.", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Wyłącz sortowanie TV", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Musisz wyłączyć sortowanie TV dla kategorii używanej przez {appName}, aby zapobiec problemom z importem. Przejdź do Sabnzbd, aby to naprawić.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Włącz foldery zadań", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} preferuje, aby każde pobranie miało osobny folder. Z dopisanym * do Folder/Path Sabnzbd nie utworzy tych folderów zadań. Przejdź do Sabnzbd, aby to naprawić.", + "DownloadClientSabnzbdValidationUnknownVersion": "Nieznana wersja: {rawVersion}", + "DownloadClientSeriesTagHelpText": "Używaj tego klienta pobierania tylko dla seriali z co najmniej jednym pasującym tagiem. Pozostaw puste, aby używać dla wszystkich seriali.", + "DownloadClientSettings": "Ustawienia klienta pobierania", + "DownloadClientSettingsAddPaused": "Dodaj wstrzymane", + "DownloadClientSettingsCategoryHelpText": "Dodanie kategorii specyficznej dla {appName} pozwala uniknąć konfliktów z niepowiązanymi pobraniami spoza {appName}. Użycie kategorii jest opcjonalne, ale zdecydowanie zalecane.", + "DownloadClientSettingsCategorySubFolderHelpText": "Dodanie kategorii specyficznej dla {appName} pozwala uniknąć konfliktów z niepowiązanymi pobraniami spoza {appName}. Użycie kategorii jest opcjonalne, ale zdecydowanie zalecane. Tworzy podkatalog [category] w katalogu wyjściowym.", + "DownloadClientSettingsDestinationHelpText": "Ręcznie określa miejsce docelowe pobierania, pozostaw puste aby użyć domyślnego", + "DownloadClientSettingsInitialState": "Stan początkowy", + "DownloadClientSettingsInitialStateHelpText": "Stan początkowy torrentów dodawanych do {clientName}", + "DownloadClientSettingsOlderPriority": "Priorytet starszych", + "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Priorytet używany podczas pobierania odcinków wyemitowanych ponad 14 dni temu", + "DownloadClientSettingsPostImportCategoryHelpText": "Kategoria, którą {appName} ustawi po zaimportowaniu pobrania. {appName} nie usunie torrentów w tej kategorii nawet po zakończeniu seedowania. Pozostaw puste, aby zachować tę samą kategorię.", + "DownloadClientSettingsRecentPriority": "Priorytet nowszych", + "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Priorytet używany podczas pobierania odcinków wyemitowanych w ciągu ostatnich 14 dni", + "DownloadClientSettingsUrlBaseHelpText": "Dodaje prefiks do URL klienta {clientName}, np. {url}", + "DownloadClientSettingsUseSslHelpText": "Użyj bezpiecznego połączenia przy łączeniu z {clientName}", + "DownloadClientSortingHealthCheckMessage": "Klient pobierania {downloadClientName} ma włączone sortowanie {sortingMode} dla kategorii {appName}. Powinieneś wyłączyć sortowanie w kliencie pobierania, aby uniknąć problemów z importem.", + "DownloadClientStatusAllClientHealthCheckMessage": "Wszyscy klienci pobierania są niedostępni z powodu błędów", + "DownloadClientStatusSingleClientHealthCheckMessage": "Klienci pobierania niedostępni z powodu błędów: {downloadClientNames}", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Dodaje prefiks do URL RPC klienta {clientName}, np. {url}, domyślnie '{defaultUrl}'", + "DownloadClientTriblerProviderMessage": "Integracja z tribler jest wysoce eksperymentalna. Testowano z wersją {clientName} {clientVersionRange}.", + "DownloadClientTriblerSettingsAnonymityLevel": "Poziom anonimowości", + "DownloadClientTriblerSettingsAnonymityLevelHelpText": "Liczba proxy używanych podczas pobierania treści. Aby wyłączyć, ustaw 0. Proxy zmniejszają prędkość pobierania/wysyłania. Zobacz {url}", + "DownloadClientTriblerSettingsApiKeyHelpText": "[api].key z pliku triblerd.conf", + "DownloadClientTriblerSettingsDirectoryHelpText": "Opcjonalna lokalizacja pobrań, pozostaw puste aby użyć domyślnej lokalizacji Tribler", + "DownloadClientTriblerSettingsSafeSeeding": "Bezpieczne seedowanie", + "DownloadClientTriblerSettingsSafeSeedingHelpText": "Po włączeniu seedowanie odbywa się tylko przez proxy.", + "DownloadClientUTorrentProviderMessage": "uTorrent ma historię dołączania cryptominerów, malware i reklam, zdecydowanie zachęcamy do wybrania innego klienta.", + "DownloadClientUTorrentTorrentStateError": "uTorrent zgłasza błąd", + "DownloadClientUnavailable": "Klient pobierania niedostępny", + "DownloadClientValidationApiKeyIncorrect": "Nieprawidłowy klucz API", + "DownloadClientValidationApiKeyRequired": "Wymagany klucz API", + "DownloadClientValidationAuthenticationFailure": "Błąd uwierzytelniania", + "DownloadClientValidationAuthenticationFailureDetail": "Zweryfikuj swoje dane logowania. Sprawdź też, czy host uruchamiający {appName} nie jest zablokowany przed dostępem do {clientName} przez ograniczenia WhiteList w konfiguracji {clientName}.", + "DownloadClientValidationCategoryMissing": "Kategoria nie istnieje", + "DownloadClientValidationCategoryMissingDetail": "Wprowadzona kategoria nie istnieje w {clientName}. Najpierw utwórz ją w {clientName}.", + "DownloadClientValidationErrorVersion": "Wersja {clientName} powinna wynosić co najmniej {requiredVersion}. Zgłoszona wersja to {reportedVersion}", + "DownloadClientValidationGroupMissing": "Grupa nie istnieje", + "DownloadClientValidationGroupMissingDetail": "Wprowadzona grupa nie istnieje w {clientName}. Najpierw utwórz ją w {clientName}.", + "DownloadClientValidationSslConnectFailure": "Nie można połączyć się przez SSL", + "DownloadClientValidationSslConnectFailureDetail": "{appName} nie może połączyć się z {clientName} przez SSL. Problem może być związany z komputerem. Spróbuj skonfigurować {appName} i {clientName} tak, aby nie używały SSL.", + "DownloadClientValidationTestNzbs": "Nie udało się pobrać listy NZB: {exceptionMessage}", + "DownloadClientValidationTestTorrents": "Nie udało się pobrać listy torrentów: {exceptionMessage}", + "DownloadClientValidationUnableToConnect": "Nie można połączyć się z {clientName}", + "DownloadClientValidationUnableToConnectDetail": "Sprawdź nazwę hosta i port.", + "DownloadClientValidationUnknownException": "Nieznany wyjątek: {exception}", + "DownloadClientValidationVerifySsl": "Zweryfikuj ustawienia SSL", + "DownloadClientValidationVerifySslDetail": "Zweryfikuj konfigurację SSL po stronie {clientName} i {appName}", + "DownloadClientVuzeValidationErrorVersion": "Wersja protokołu nie jest obsługiwana, użyj Vuze 5.0.0.0 lub nowszego z wtyczką Vuze Web Remote.", "DownloadClients": "Klienci pobierania", + "DownloadClientsLoadError": "Nie można wczytać klientów pobierania", + "DownloadClientsSettingsSummary": "Klienci pobierania, obsługa pobierania i mapowania ścieżek zdalnych", + "DownloadFailed": "Pobieranie nieudane", + "DownloadFailedEpisodeTooltip": "Pobieranie odcinka nie powiodło się", + "DownloadIgnored": "Pobranie zignorowane", + "DownloadIgnoredEpisodeTooltip": "Pobieranie odcinka zignorowano", + "DownloadPropersAndRepacks": "Propers i Repacks", + "DownloadPropersAndRepacksHelpText": "Czy automatycznie aktualizować do Propers/Repacks", + "DownloadPropersAndRepacksHelpTextCustomFormat": "Użyj opcji 'Nie preferuj', aby sortować według wyniku formatu niestandardowego ponad Propers/Repacks", + "DownloadPropersAndRepacksHelpTextWarning": "Użyj formatów niestandardowych do automatycznych aktualizacji do Propers/Repacks", + "DownloadStationStatusExtracting": "Wypakowywanie: {progress}%", + "DownloadWarning": "Ostrzeżenie pobierania: {warningMessage}", + "Downloaded": "Pobrano", + "Downloading": "Pobieranie", + "Duplicate": "Duplikat", + "Duration": "Czas trwania", + "Edit": "Edytuj", + "EditAutoTag": "Edytuj automatyczny tag", + "EditConditionImplementation": "Edytuj warunek - {implementationName}", + "EditConnectionImplementation": "Edytuj połączenie - {implementationName}", + "EditCustomFormat": "Edytuj format niestandardowy", + "EditDelayProfile": "Edytuj profil opóźnienia", + "EditDownloadClientImplementation": "Edytuj klienta pobierania - {implementationName}", + "EditGroups": "Edytuj grupy", + "EditImportListExclusion": "Edytuj wykluczenie listy importu", + "EditImportListImplementation": "Edytuj listę importu - {implementationName}", + "EditIndexerImplementation": "Edytuj indekser - {implementationName}", + "EditListExclusion": "Edytuj wykluczenie listy", + "EditMetadata": "Edytuj metadane {metadataType}", + "EditQualityProfile": "Edytuj profil jakości", + "EditReleaseProfile": "Edytuj profil wydań", + "EditRemotePathMapping": "Edytuj mapowanie ścieżki zdalnej", + "EditRestriction": "Edytuj ograniczenie", + "EditSelectedCustomFormats": "Edytuj zaznaczone formaty niestandardowe", + "EditSelectedDownloadClients": "Edytuj zaznaczonych klientów pobierania", + "EditSelectedImportLists": "Edytuj zaznaczone listy importu", + "EditSelectedIndexers": "Edytuj zaznaczone indeksery", + "EditSelectedSeries": "Edytuj zaznaczone seriale", + "EditSeries": "Edytuj serial", + "EditSeriesModalHeader": "Edytuj - {title}", + "EditSizes": "Edytuj rozmiary", + "Empty": "Puste", + "EmptyRootFolderTooltip": "Ten folder główny nie zawiera żadnych plików ani folderów. {appName} nie będzie skanować zmian ani tworzyć pustych folderów seriali.", + "Enable": "Włącz", + "EnableAutomaticAdd": "Włącz automatyczne dodawanie", + "EnableAutomaticAddSeriesHelpText": "Dodawaj seriale z tej listy do {appName}, gdy synchronizacje są wykonywane przez UI lub przez {appName}", + "EnableAutomaticSearch": "Włącz wyszukiwanie automatyczne", + "EnableAutomaticSearchHelpText": "Będzie używane, gdy automatyczne wyszukiwania są wykonywane przez UI lub przez {appName}", + "EnableAutomaticSearchHelpTextWarning": "Będzie używane przy wyszukiwaniu interaktywnym", + "EnableColorImpairedMode": "Włącz tryb dla osób z zaburzeniami rozpoznawania barw", + "EnableColorImpairedModeHelpText": "Zmodyfikowany styl, aby umożliwić osobom z zaburzeniami rozpoznawania barw łatwiejsze rozróżnianie informacji oznaczonych kolorami", + "EnableCompletedDownloadHandlingHelpText": "Automatycznie importuj ukończone pobrania z klienta pobierania", + "EnableHelpText": "Włącz tworzenie plików metadanych dla tego typu metadanych", + "EnableInteractiveSearch": "Włącz wyszukiwanie interaktywne", + "EnableInteractiveSearchHelpText": "Będzie używane przy wyszukiwaniu interaktywnym", + "EnableInteractiveSearchHelpTextWarning": "Wyszukiwanie nie jest obsługiwane przez ten indekser", + "EnableMediaInfoHelpText": "Wyodrębnij informacje o wideo, takie jak rozdzielczość, czas trwania i kodeki. Wymaga to od {appName} odczytu fragmentów pliku, co może powodować wysoką aktywność dysku lub sieci podczas skanowania.", + "EnableMetadataHelpText": "Włącz tworzenie plików metadanych dla tego typu metadanych", + "EnableProfile": "Włącz profil", + "EnableProfileHelpText": "Zaznacz, aby włączyć profil wydań", + "EnableRss": "Włącz RSS", + "EnableRssHelpText": "Będzie używane, gdy {appName} okresowo wyszukuje wydania przez synchronizację RSS", + "EnableSsl": "Włącz SSL", + "EnableSslHelpText": "Aby zadziałało, wymagany restart uruchomiony jako administrator", + "Enabled": "Włączone", + "Ended": "Zakończony", + "EndedOnly": "Tylko zakończone", + "EndedSeriesDescription": "Nie są oczekiwane żadne dodatkowe odcinki ani sezony", + "Episode": "Odcinek", + "EpisodeAirDate": "Data emisji odcinka", + "EpisodeCount": "Liczba odcinków", + "EpisodeDownloaded": "Odcinek pobrany", + "EpisodeFileDeleted": "Plik odcinka usunięty", + "EpisodeFileDeletedTooltip": "Plik odcinka usunięty", + "EpisodeFileMissingTooltip": "Brak pliku odcinka", + "EpisodeFileRenamed": "Zmieniono nazwę pliku odcinka", + "EpisodeFileRenamedTooltip": "Zmieniono nazwę pliku odcinka", + "EpisodeFilesLoadError": "Nie można wczytać plików odcinków", + "EpisodeGrabbedTooltip": "Odcinek pobrano z {indexer} i wysłano do {downloadClient}", + "EpisodeHasNotAired": "Odcinek nie został jeszcze wyemitowany", + "EpisodeHistoryLoadError": "Nie można wczytać historii odcinka", + "EpisodeImported": "Odcinek zaimportowany", + "EpisodeImportedTooltip": "Odcinek pobrany pomyślnie i przechwycony z klienta pobierania", + "EpisodeInfo": "Informacje o odcinku", + "EpisodeIsDownloading": "Odcinek jest pobierany", + "EpisodeIsNotMonitored": "Odcinek nie jest monitorowany", + "EpisodeMaybePlural": "Odcinek(i)", + "EpisodeMissingAbsoluteNumber": "Odcinek nie ma bezwzględnego numeru odcinka", + "EpisodeMissingFromDisk": "Brak odcinka na dysku", + "EpisodeMonitoring": "Monitorowanie odcinków", + "EpisodeNaming": "Nazewnictwo odcinków", + "EpisodeNumbers": "Numer(y) odcinka", + "EpisodeProgress": "Postęp odcinków", + "EpisodeRequested": "Odcinek żądany", + "EpisodeSearchResultsLoadError": "Nie można wczytać wyników wyszukiwania dla tego odcinka. Spróbuj ponownie później", + "EpisodeTitle": "Tytuł odcinka", + "EpisodeTitleFootNote": "Opcjonalnie kontroluj przycinanie do maksymalnej liczby bajtów, łącznie z wielokropkiem (`...`). Obsługiwane jest przycinanie od końca (np. `{Episode Title:30}`) lub od początku (np. `{Episode Title:-30}`). Tytuły odcinków będą automatycznie przycinane do ograniczeń systemu plików, jeśli to konieczne.", + "EpisodeTitleMaybePlural": "Tytuł(y) odcinka", + "EpisodeTitleRequired": "Wymagany tytuł odcinka", + "EpisodeTitleRequiredHelpText": "Zapobiegaj importowi przez maksymalnie 48 godzin, jeśli tytuł odcinka jest użyty w formacie nazwy i tytuł odcinka to TBA", + "EpisodeTitles": "Tytuły odcinków", + "Episodes": "Odcinki", + "EpisodesInSeason": "{episodeCount} odcinków w sezonie", + "EpisodesLoadError": "Nie można wczytać odcinków", + "EpisodesMonitoredStatus": "Status monitorowania odcinków", + "Error": "Błąd", + "ErrorLoadingContent": "Wystąpił błąd podczas ładowania tej zawartości", + "ErrorLoadingContents": "Błąd ładowania zawartości", + "ErrorLoadingItem": "Wystąpił błąd podczas ładowania tego elementu", + "ErrorLoadingPage": "Wystąpił błąd podczas ładowania tej strony", + "ErrorRestoringBackup": "Błąd przywracania kopii zapasowej", + "EventType": "Typ zdarzenia", + "Events": "Zdarzenia", + "Example": "Przykład", + "Exception": "Wyjątek", + "ExcludeSpecials": "Wyklucz specjalne", + "ExcludeUnknownSeriesItems": "Wyklucz nieznane elementy seriali", + "ExcludedReleaseProfile": "Wykluczony profil wydań", + "ExcludedReleaseProfiles": "Wykluczone profile wydań", + "ExcludedTags": "Wykluczone tagi", + "Existing": "Istniejące", + "ExistingSeries": "Istniejące seriale", + "ExistingTag": "Istniejący tag", + "ExpandAll": "Rozwiń wszystko", + "ExportCustomFormat": "Eksportuj format niestandardowy", + "Extend": "Rozszerz", + "External": "Zewnętrzne", + "ExternalUpdater": "{appName} jest skonfigurowany do używania zewnętrznego mechanizmu aktualizacji", + "ExtraFileExtensionsHelpText": "Lista dodatkowych rozszerzeń plików do importu oddzielona przecinkami (.nfo zostanie zaimportowane jako .nfo-orig)", + "ExtraFileExtensionsHelpTextsExamples": "Przykłady: '.sub, .nfo' lub 'sub,nfo'", + "Failed": "Nieudane", + "FailedAt": "Nieudane o: {date}", + "FailedToFetchSettings": "Nie udało się pobrać ustawień", + "FailedToFetchUpdates": "Nie udało się pobrać aktualizacji", + "FailedToLoadCustomFiltersFromApi": "Nie udało się wczytać filtrów niestandardowych z API", + "FailedToLoadQualityProfilesFromApi": "Nie udało się wczytać profili jakości z API", + "FailedToLoadSeriesFromApi": "Nie udało się wczytać seriali z API", + "FailedToLoadSonarr": "Nie udało się wczytać {appName}", + "FailedToLoadSystemStatusFromApi": "Nie udało się wczytać stanu systemu z API", + "FailedToLoadTagsFromApi": "Nie udało się wczytać tagów z API", + "FailedToLoadTranslationsFromApi": "Nie udało się wczytać tłumaczeń z API", + "FailedToLoadUiSettingsFromApi": "Nie udało się wczytać ustawień interfejsu z API", + "Fallback": "Zapasowe", + "False": "Fałsz", + "FavoriteFolderAdd": "Dodaj ulubiony folder", + "FavoriteFolderRemove": "Usuń ulubiony folder", + "FavoriteFolders": "Ulubione foldery", + "FeatureRequests": "Propozycje funkcji", + "File": "Plik", + "FileBrowser": "Przeglądarka plików", + "FileBrowserPlaceholderText": "Zacznij pisać lub wybierz ścieżkę poniżej", + "FileManagement": "Zarządzanie plikami", + "FileNameTokens": "Tokeny nazwy pliku", + "FileNames": "Nazwy plików", + "FileSize": "Rozmiar pliku", + "Filename": "Nazwa pliku", + "Files": "Pliki", + "Filter": "Filtr", + "FilterContains": "zawiera", + "FilterDoesNotContain": "nie zawiera", + "FilterDoesNotEndWith": "nie kończy się na", + "FilterDoesNotStartWith": "nie zaczyna się od", + "FilterEndsWith": "kończy się na", + "FilterEpisodesPlaceholder": "Filtruj odcinki po tytule lub numerze", + "FilterEqual": "równe", + "FilterGreaterThan": "większe niż", + "FilterGreaterThanOrEqual": "większe lub równe", + "FilterInLast": "w ostatnich", + "FilterInNext": "w następnych", + "FilterIs": "jest", + "FilterIsAfter": "jest po", + "FilterIsBefore": "jest przed", + "FilterIsNot": "nie jest", + "FilterLessThan": "mniejsze niż", + "FilterLessThanOrEqual": "mniejsze lub równe", + "FilterNotEqual": "nierówne", + "FilterNotInLast": "nie w ostatnich", + "FilterNotInNext": "nie w następnych", + "FilterSeriesPlaceholder": "Filtruj seriale", + "FilterStartsWith": "zaczyna się od", + "Filters": "Filtry", + "FinaleTooltip": "Finał serialu lub sezonu", + "FirstDayOfWeek": "Pierwszy dzień tygodnia", + "Fixed": "Stałe", + "Folder": "Folder", + "FolderNameTokens": "Tokeny nazwy folderu", + "Folders": "Foldery", + "Forecast": "Prognoza", + "FormatAgeDay": "dzień", + "FormatAgeDays": "dni", + "FormatAgeHour": "godzina", + "FormatAgeHours": "godziny", + "FormatAgeMinute": "minuta", + "FormatAgeMinutes": "minuty", + "FormatDateTime": "{formattedDate} {formattedTime}", + "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", + "FormatRuntimeHours": "{hours}h", + "FormatRuntimeMinutes": "{minutes}m", + "FormatShortTimeSpanHours": "{hours} godz.", + "FormatShortTimeSpanMinutes": "{minutes} min.", + "FormatShortTimeSpanSeconds": "{seconds} sek.", + "FormatTimeSpanDays": "{days}d {time}", + "Formats": "Formaty", + "Forums": "Fora", + "FreeSpace": "Wolne miejsce", + "Friday": "Piątek", + "From": "Od", + "FullColorEvents": "Pełnokolorowe wydarzenia", + "FullColorEventsHelpText": "Zmienia styl tak, aby kolor statusu obejmował całe wydarzenie, a nie tylko lewą krawędź. Nie dotyczy Agendy", + "FullSeason": "Pełny sezon", "General": "Ogólne", - "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.", - "RemoveFromDownloadClient": "Usuń z Klienta Pobierania", - "StartupDirectory": "Katalog Startowy", + "GeneralSettings": "Ustawienia ogólne", + "GeneralSettingsLoadError": "Nie można wczytać ustawień ogólnych", + "GeneralSettingsSummary": "Port, SSL, nazwa użytkownika/hasło, proxy, analityka i aktualizacje", + "Genres": "Gatunki", + "Global": "Globalne", + "Grab": "Pobierz", + "GrabId": "ID pobrania", + "GrabRelease": "Pobierz wydanie", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} nie był w stanie określić, którego serialu i odcinka dotyczy to wydanie. {appName} może nie być w stanie automatycznie zaimportować tego wydania. Czy chcesz pobrać '{title}'?", + "GrabSelected": "Wybierz zaznaczone", + "Grabbed": "Wybrane", + "GrabbedAt": "Wybrane o: {date}", + "Group": "Grupa", + "HardlinkCopyFiles": "Dowiązanie twarde/kopiuj pliki", + "HasMissingSeason": "Ma brakujący sezon", + "HasUnmonitoredSeason": "Ma niemonitorowany sezon", + "Health": "Zdrowie", + "HealthIssue": "1 problem ze stanem", + "HealthIssues": "{count} problemów ze stanem", + "HealthMessagesInfoBox": "Więcej informacji o przyczynie tych komunikatów kontroli stanu znajdziesz, klikając link wiki (ikona książki) na końcu wiersza lub sprawdzając [logi]({link}). Jeśli masz trudność z interpretacją tych komunikatów, możesz skontaktować się z naszym wsparciem przez linki poniżej.", + "Here": "tutaj", + "HiddenClickToShow": "Ukryte, kliknij aby pokazać", + "HideAdvanced": "Ukryj zaawansowane", + "HideEpisodes": "Ukryj odcinki", + "History": "Historia", + "HistoryLoadError": "Nie można wczytać historii", + "HistoryModalHeaderSeason": "Historia {season}", + "HistorySeason": "Pokaż historię dla tego sezonu", + "HomePage": "Strona główna", + "Host": "Host", + "Hostname": "Nazwa hosta", + "HourShorthand": "h", + "HttpHttps": "HTTP(S)", + "ICalFeed": "Kanał iCal", + "ICalFeedHelpText": "Skopiuj ten URL do klienta(ów) lub kliknij, aby subskrybować, jeśli Twoja przeglądarka obsługuje webcal", + "ICalIncludeUnmonitoredEpisodesHelpText": "Uwzględniaj niemonitorowane odcinki w kanale iCal", + "ICalLink": "Link iCal", + "ICalSeasonPremieresOnlyHelpText": "W kanale będzie tylko pierwszy odcinek sezonu", + "ICalShowAsAllDayEvents": "Pokaż jako wydarzenia całodniowe", + "ICalShowAsAllDayEventsHelpText": "Wydarzenia będą wyświetlane w kalendarzu jako całodniowe", + "ICalTagsSeriesHelpText": "Kanał będzie zawierał tylko seriale z co najmniej jednym pasującym tagiem", + "IRC": "IRC", + "IRCLinkText": "#sonarr na Libera", + "IconForCutoffUnmet": "Ikona dla nieosiągniętego progu", + "IconForCutoffUnmetHelpText": "Pokazuj ikonę przy plikach, gdy próg nie został osiągnięty", + "IconForFinales": "Ikona dla finałów", + "IconForFinalesHelpText": "Pokazuj ikonę dla finałów serialu/sezonu na podstawie dostępnych informacji o odcinkach", + "IconForSpecials": "Ikona dla odcinków specjalnych", + "IconForSpecialsHelpText": "Pokazuj ikonę dla odcinków specjalnych (sezon 0)", + "IgnoreDownload": "Ignoruj pobranie", + "IgnoreDownloadHint": "Powoduje, że {appName} przestaje dalej przetwarzać to pobranie", + "IgnoreDownloads": "Ignoruj pobrania", + "IgnoreDownloadsHint": "Powoduje, że {appName} przestaje dalej przetwarzać te pobrania", + "Ignored": "Zignorowane", + "IgnoredAddresses": "Ignorowane adresy", + "ImageBanner": "baner", + "ImageFanart": "fanart", + "ImagePoster": "plakat", + "ImageSeason": "sezon", + "Images": "Obrazy", + "ImdbId": "ID IMDb", + "Implementation": "Implementacja", + "Import": "Importuj", + "ImportCountSeries": "Importuj {selectedCount} seriali", + "ImportCustomFormat": "Importuj format niestandardowy", + "ImportErrors": "Błędy importu", + "ImportExistingSeries": "Importuj istniejące seriale", + "ImportExtraFiles": "Importuj dodatkowe pliki", + "ImportExtraFilesEpisodeHelpText": "Importuj pasujące dodatkowe pliki (napisy, nfo itp.) po zaimportowaniu pliku odcinka", + "ImportFailed": "Import nieudany: {sourceTitle}", + "ImportList": "Lista importu", + "ImportListExclusions": "Wykluczenia listy importu", + "ImportListExclusionsLoadError": "Nie można wczytać wykluczeń listy importu", + "ImportListRootFolderMissingRootHealthCheckMessage": "Brak folderu głównego dla list importu: {rootFolderInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Brak wielu folderów głównych dla list importu: {rootFolderInfo}", + "ImportListSearchForMissingEpisodes": "Szukaj brakujących odcinków", + "ImportListSearchForMissingEpisodesHelpText": "Po dodaniu serialu do {appName} automatycznie wyszukaj brakujące odcinki", + "ImportListSettings": "Ustawienia listy importu", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Wszystkie listy wymagają ręcznej interakcji z powodu możliwych częściowych pobrań", + "ImportListStatusAllUnavailableHealthCheckMessage": "Wszystkie listy są niedostępne z powodu błędów", + "ImportListStatusUnavailableHealthCheckMessage": "Listy niedostępne z powodu błędów: {importListNames}", + "ImportLists": "Listy importu", + "ImportListsAniListSettingsAuthenticateWithAniList": "Uwierzytelnij przez AniList", + "ImportListsAniListSettingsImportCancelled": "Import anulowanych", + "ImportListsAniListSettingsImportCancelledHelpText": "Media: serial anulowany", + "ImportListsAniListSettingsImportCompleted": "Import ukończonych", + "ImportListsAniListSettingsImportCompletedHelpText": "Lista: ukończono oglądanie", + "ImportListsAniListSettingsImportDropped": "Import porzuconych", + "ImportListsAniListSettingsImportDroppedHelpText": "Lista: porzucone", + "ImportListsAniListSettingsImportFinished": "Import zakończonych", + "ImportListsAniListSettingsImportFinishedHelpText": "Media: wszystkie odcinki zostały wyemitowane", + "ImportListsAniListSettingsImportHiatus": "Import wstrzymanych", + "ImportListsAniListSettingsImportHiatusHelpText": "Media: serial na hiatusie", + "ImportListsAniListSettingsImportNotYetReleased": "Import jeszcze niewydanych", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Media: emisja jeszcze się nie rozpoczęła", + "ImportListsAniListSettingsImportPaused": "Import wstrzymanych pozycji", + "ImportListsAniListSettingsImportPausedHelpText": "Lista: wstrzymane", + "ImportListsAniListSettingsImportPlanning": "Import planowanych", + "ImportListsAniListSettingsImportPlanningHelpText": "Lista: planuję obejrzeć", + "ImportListsAniListSettingsImportReleasing": "Import aktualnie emitowanych", + "ImportListsAniListSettingsImportReleasingHelpText": "Media: obecnie emitowane są nowe odcinki", + "ImportListsAniListSettingsImportRepeating": "Import powtórnie oglądanych", + "ImportListsAniListSettingsImportRepeatingHelpText": "Lista: obecnie oglądane ponownie", + "ImportListsAniListSettingsImportWatching": "Import oglądanych", + "ImportListsAniListSettingsImportWatchingHelpText": "Lista: obecnie oglądane", + "ImportListsAniListSettingsUsernameHelpText": "Nazwa użytkownika listy, z której importować", + "ImportListsCustomListSettingsName": "Lista niestandardowa", + "ImportListsCustomListSettingsUrl": "URL listy", + "ImportListsCustomListSettingsUrlHelpText": "URL listy seriali", + "ImportListsCustomListValidationAuthenticationFailure": "Błąd uwierzytelniania", + "ImportListsCustomListValidationConnectionError": "Nie można wykonać żądania do tego URL. StatusCode: {exceptionStatusCode}", + "ImportListsImdbSettingsListId": "ID listy", + "ImportListsImdbSettingsListIdHelpText": "ID listy IMDb (np. ls12345678)", + "ImportListsLoadError": "Nie można wczytać list importu", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Uwierzytelnij przez MyAnimeList", + "ImportListsMyAnimeListSettingsListStatus": "Status listy", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Typ listy, z której chcesz importować; ustaw 'All' dla wszystkich list", + "ImportListsMyAnimeListSettingsScore": "Minimalny wynik", + "ImportListsMyAnimeListSettingsScoreHelpText": "Minimalny wynik seriali do importu", + "ImportListsPlexSettingsAuthenticateWithPlex": "Uwierzytelnij przez Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Lista do obejrzenia Plex", + "ImportListsPlexSettingsWatchlistRSSName": "RSS listy do obejrzenia Plex", + "ImportListsSettingsAccessToken": "Token dostępu", + "ImportListsSettingsAuthUser": "Użytkownik auth", + "ImportListsSettingsExpires": "Wygasa", + "ImportListsSettingsRefreshToken": "Token odświeżania", + "ImportListsSettingsRssUrl": "URL RSS", + "ImportListsSettingsSummary": "Import z innej instancji {appName} lub list Trakt oraz zarządzanie wykluczeniami list", + "ImportListsSimklSettingsAuthenticatewithSimkl": "Uwierzytelnij przez Simkl", + "ImportListsSimklSettingsListType": "Typ listy", + "ImportListsSimklSettingsListTypeHelpText": "Typ listy, z której chcesz importować", + "ImportListsSimklSettingsName": "Lista do obejrzenia użytkownika Simkl", + "ImportListsSimklSettingsShowType": "Typ serialu", + "ImportListsSimklSettingsShowTypeHelpText": "Typ serialu, który chcesz importować", + "ImportListsSimklSettingsUserListTypeCompleted": "Ukończone", + "ImportListsSimklSettingsUserListTypeDropped": "Porzucone", + "ImportListsSimklSettingsUserListTypeHold": "Wstrzymane", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Planowane do obejrzenia", + "ImportListsSimklSettingsUserListTypeWatching": "Oglądane", + "ImportListsSonarrSettingsApiKeyHelpText": "Klucz API instancji {appName}, z której importować", + "ImportListsSonarrSettingsFullUrl": "Pełny URL", + "ImportListsSonarrSettingsFullUrlHelpText": "URL instancji {appName}, łącznie z portem, z której importować", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Profile jakości z instancji źródłowej do importu", + "ImportListsSonarrSettingsRootFoldersHelpText": "Foldery główne z instancji źródłowej do importu", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Synchronizuj monitorowanie sezonów", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Synchronizuj monitorowanie sezonów z instancji {appName}; jeśli włączone, opcja 'Monitor' będzie ignorowana", + "ImportListsSonarrSettingsTagsHelpText": "Tagi z instancji źródłowej do importu", + "ImportListsSonarrValidationInvalidUrl": "URL {appName} jest nieprawidłowy, czy brakuje bazowego URL?", + "ImportListsTraktSettingsAdditionalParameters": "Dodatkowe parametry", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Dodatkowe parametry API Trakt", + "ImportListsTraktSettingsAuthenticateWithTrakt": "Uwierzytelnij przez Trakt", + "ImportListsTraktSettingsGenres": "Gatunki", + "ImportListsTraktSettingsGenresSeriesHelpText": "Filtruj seriale według slugów gatunków Trakt (oddzielone przecinkami), tylko dla list popularnych", + "ImportListsTraktSettingsLimit": "Limit", + "ImportListsTraktSettingsLimitSeriesHelpText": "Ogranicz liczbę pobieranych seriali", + "ImportListsTraktSettingsListName": "Nazwa listy", + "ImportListsTraktSettingsListNameHelpText": "Nazwa listy do importu; lista musi być publiczna lub musisz mieć do niej dostęp", + "ImportListsTraktSettingsListType": "Typ listy", + "ImportListsTraktSettingsListTypeHelpText": "Typ listy, z której chcesz importować", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Oczekiwane seriale", + "ImportListsTraktSettingsPopularListTypePopularShows": "Popularne seriale", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Polecane seriale wszech czasów", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Polecane seriale miesiąca", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Polecane seriale tygodnia", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Polecane seriale roku", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Najczęściej oglądane seriale wszech czasów", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Najczęściej oglądane seriale miesiąca", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Najczęściej oglądane seriale tygodnia", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Najczęściej oglądane seriale roku", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Trendujące seriale", + "ImportListsTraktSettingsPopularName": "Popularna lista Trakt", + "ImportListsTraktSettingsRating": "Ocena", + "ImportListsTraktSettingsRatingSeriesHelpText": "Filtruj seriale według zakresu ocen (0-100)", + "ImportListsTraktSettingsUserListName": "Użytkownik Trakt", + "ImportListsTraktSettingsUserListTypeCollection": "Lista kolekcji użytkownika", + "ImportListsTraktSettingsUserListTypeWatch": "Lista do obejrzenia użytkownika", + "ImportListsTraktSettingsUserListTypeWatched": "Lista obejrzanych użytkownika", + "ImportListsTraktSettingsUserListUsernameHelpText": "Nazwa użytkownika listy, z której importować (pozostaw puste, aby użyć użytkownika uwierzytelnionego)", + "ImportListsTraktSettingsUsernameHelpText": "Nazwa użytkownika listy, z której importować", + "ImportListsTraktSettingsWatchListSorting": "Sortowanie listy do obejrzenia", + "ImportListsTraktSettingsWatchListSortingHelpText": "Jeśli typ listy to 'Do obejrzenia', wybierz kolejność sortowania listy", + "ImportListsTraktSettingsWatchedListFilter": "Filtr listy obejrzanych", + "ImportListsTraktSettingsWatchedListFilterSeriesHelpText": "Jeśli typ listy to 'Obejrzane', wybierz typ serialu, który chcesz importować", + "ImportListsTraktSettingsWatchedListTypeAll": "Wszystkie", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% obejrzane", + "ImportListsTraktSettingsWatchedListTypeInProgress": "W trakcie", + "ImportListsTraktSettingsYears": "Lata", + "ImportListsTraktSettingsYearsSeriesHelpText": "Filtruj seriale według roku lub zakresu lat", + "ImportListsValidationInvalidApiKey": "Klucz API jest nieprawidłowy", + "ImportListsValidationTestFailed": "Test został przerwany z powodu błędu: {exceptionMessage}", + "ImportListsValidationUnableToConnectException": "Nie można połączyć się z listą importu: {exceptionMessage}. Sprawdź log wokół tego błędu, aby poznać szczegóły.", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Włącz obsługę ukończonych pobrań, jeśli to możliwe", + "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Włącz obsługę ukończonych pobrań, jeśli to możliwe (Multi-Computer nieobsługiwane)", + "ImportMechanismHandlingDisabledHealthCheckMessage": "Włącz obsługę ukończonych pobrań", + "ImportScriptPath": "Ścieżka skryptu importu", + "ImportScriptPathHelpText": "Ścieżka do skryptu używanego do importowania", + "ImportSelected": "Importuj zaznaczone", + "ImportSeries": "Importuj seriale", + "ImportUsingScript": "Importuj przy użyciu skryptu", + "ImportUsingScriptHelpText": "Kopiuj pliki do importu przy użyciu skryptu (np. do transkodowania)", + "Imported": "Zaimportowano", + "ImportedTo": "Zaimportowano do", + "Importing": "Importowanie", + "IncludeCustomFormatWhenRenaming": "Uwzględnij format niestandardowy podczas zmiany nazwy", + "IncludeCustomFormatWhenRenamingHelpText": "Uwzględnij w formacie zmiany nazwy {Custom Formats}", + "IncludeHealthWarnings": "Uwzględnij ostrzeżenia stanu", + "IncludeSpecials": "Uwzględnij odcinki specjalne", + "IncludeUnmonitored": "Uwzględnij niemonitorowane", + "Indexer": "Indekser", + "IndexerDownloadClientHealthCheckMessage": "Indeksery z nieprawidłowymi klientami pobierania: {indexerNames}.", + "IndexerDownloadClientHelpText": "Określ, który klient pobierania jest używany dla pobrań z tego indeksera", + "IndexerFlags": "Flagi indeksera", + "IndexerHDBitsSettingsCategories": "Kategorie", + "IndexerHDBitsSettingsCategoriesHelpText": "Jeśli nie określono, używane są wszystkie opcje.", + "IndexerHDBitsSettingsCodecs": "Kodeki", + "IndexerHDBitsSettingsCodecsHelpText": "Jeśli nie określono, używane są wszystkie opcje.", + "IndexerHDBitsSettingsMediums": "Nośniki", + "IndexerHDBitsSettingsMediumsHelpText": "Jeśli nie określono, używane są wszystkie opcje.", + "IndexerIPTorrentsSettingsFeedUrl": "URL kanału", + "IndexerIPTorrentsSettingsFeedUrlHelpText": "Pełny URL kanału RSS wygenerowany przez IPTorrents, używający tylko wybranych kategorii (HD, SD, x264 itd.)", + "IndexerJackettAllHealthCheckMessage": "Indeksery używające nieobsługiwanego endpointu Jackett 'all': {indexerNames}", + "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Wszystkie indeksery są niedostępne z powodu błędów przez ponad 6 godzin", + "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indeksery niedostępne z powodu błędów przez ponad 6 godzin: {indexerNames}", + "IndexerOptionsLoadError": "Nie można wczytać opcji indeksera", + "IndexerPriority": "Priorytet indeksera", + "IndexerPriorityHelpText": "Priorytet indeksera od 1 (najwyższy) do 50 (najniższy). Domyślnie: 25. Używany przy pobieraniu wydań jako rozstrzygnięcie remisów dla równorzędnych wydań; {appName} nadal będzie używać wszystkich włączonych indekserów do synchronizacji RSS i wyszukiwania", + "IndexerRssNoIndexersAvailableHealthCheckMessage": "Wszystkie indeksery obsługujące RSS są tymczasowo niedostępne z powodu ostatnich błędów indekserów", + "IndexerRssNoIndexersEnabledHealthCheckMessage": "Brak dostępnych indekserów z włączoną synchronizacją RSS, {appName} nie będzie automatycznie pobierać nowych wydań", + "IndexerSearchNoAutomaticHealthCheckMessage": "Brak dostępnych indekserów z włączonym wyszukiwaniem automatycznym, {appName} nie zapewni wyników automatycznego wyszukiwania", + "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Wszystkie indeksery obsługujące wyszukiwanie są tymczasowo niedostępne z powodu ostatnich błędów indekserów", + "IndexerSearchNoInteractiveHealthCheckMessage": "Brak dostępnych indekserów z włączonym wyszukiwaniem interaktywnym, {appName} nie zapewni wyników wyszukiwania interaktywnego", + "IndexerSettings": "Ustawienia indeksera", + "IndexerSettingsAdditionalNewznabParametersHelpText": "Pamiętaj, że jeśli zmienisz kategorię, będziesz musiał dodać wymagane/ograniczające reguły dotyczące podgrup, aby uniknąć wydań w obcych językach.", + "IndexerSettingsAdditionalParameters": "Dodatkowe parametry", + "IndexerSettingsAdditionalParametersNyaa": "Dodatkowe parametry", + "IndexerSettingsAllowZeroSize": "Zezwalaj na zerowy rozmiar", + "IndexerSettingsAllowZeroSizeHelpText": "Włączenie tego pozwoli używać kanałów, które nie podają rozmiaru wydania, ale uważaj: kontrole związane z rozmiarem nie będą wykonywane.", + "IndexerSettingsAnimeCategories": "Kategorie anime", + "IndexerSettingsAnimeCategoriesHelpText": "Lista rozwijana, pozostaw puste aby wyłączyć anime", + "IndexerSettingsAnimeStandardFormatSearch": "Wyszukiwanie anime w standardowym formacie", + "IndexerSettingsAnimeStandardFormatSearchHelpText": "Wyszukuj anime także z użyciem standardowej numeracji", + "IndexerSettingsApiPath": "Ścieżka API", + "IndexerSettingsApiPathHelpText": "Ścieżka do API, zwykle {url}", + "IndexerSettingsApiUrl": "URL API", + "IndexerSettingsApiUrlHelpText": "Nie zmieniaj tego, chyba że wiesz co robisz, ponieważ Twój klucz API zostanie wysłany do tego hosta.", + "IndexerSettingsCategories": "Kategorie", + "IndexerSettingsCategoriesHelpText": "Lista rozwijana, pozostaw puste aby wyłączyć standardowe/dzienne seriale", + "IndexerSettingsCookie": "Cookie", + "IndexerSettingsCookieHelpText": "Jeśli strona wymaga cookie logowania do dostępu do RSS, musisz je pobrać przez przeglądarkę.", + "IndexerSettingsFailDownloads": "Nieudane pobrania", + "IndexerSettingsFailDownloadsHelpText": "Podczas przetwarzania ukończonych pobrań {appName} potraktuje wybrane typy plików jako nieudane pobrania.", + "IndexerSettingsMinimumSeeders": "Minimalna liczba seedów", + "IndexerSettingsMinimumSeedersHelpText": "Wymagana minimalna liczba seedów.", + "IndexerSettingsMultiLanguageRelease": "Wiele języków", + "IndexerSettingsMultiLanguageReleaseHelpText": "Jakie języki zwykle występują w wielojęzycznym wydaniu na tym indekserze?", + "IndexerSettingsPasskey": "Passkey", + "IndexerSettingsRejectBlocklistedTorrentHashes": "Odrzucaj hashe torrentów z czarnej listy podczas pobierania", + "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Jeśli torrent jest zablokowany po hashu, niektóre indeksery mogą nie odrzucić go poprawnie podczas RSS/Search. Włączenie tej opcji pozwoli odrzucić go po pobraniu torrenta, ale przed wysłaniem do klienta.", + "IndexerSettingsRssUrl": "URL RSS", + "IndexerSettingsRssUrlHelpText": "Wprowadź URL kanału RSS zgodnego z {indexer}", + "IndexerSettingsSeasonPackSeedGoal": "Cel seedowania dla paczek sezonu", + "IndexerSettingsSeasonPackSeedGoalHelpText": "Wybierz, czy używać innych celów seedowania dla paczek sezonu", + "IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Użyj celów dla paczek sezonu", + "IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Użyj standardowych celów", + "IndexerSettingsSeasonPackSeedRatio": "Współczynnik seedowania paczki sezonu", + "IndexerSettingsSeasonPackSeedRatioHelpText": "Współczynnik, który torrent paczki sezonu powinien osiągnąć przed zatrzymaniem; puste używa domyślnego klienta pobierania. Współczynnik powinien wynosić co najmniej 1.0 i być zgodny z zasadami indeksera", + "IndexerSettingsSeasonPackSeedTime": "Czas seedowania paczki sezonu", + "IndexerSettingsSeasonPackSeedTimeHelpText": "Czas, przez jaki torrent paczki sezonu powinien seedować przed zatrzymaniem; puste używa domyślnego klienta pobierania", + "IndexerSettingsSeedRatio": "Współczynnik seedowania", + "IndexerSettingsSeedRatioHelpText": "Współczynnik, który torrent powinien osiągnąć przed zatrzymaniem; puste używa domyślnego klienta pobierania. Współczynnik powinien wynosić co najmniej 1.0 i być zgodny z zasadami indeksera", + "IndexerSettingsSeedTime": "Czas seedowania", + "IndexerSettingsSeedTimeHelpText": "Czas, przez jaki torrent powinien seedować przed zatrzymaniem; puste używa domyślnego klienta pobierania", + "IndexerSettingsWebsiteUrl": "URL strony", + "IndexerStatusAllUnavailableHealthCheckMessage": "Wszystkie indeksery są niedostępne z powodu błędów", + "IndexerStatusUnavailableHealthCheckMessage": "Indeksery niedostępne z powodu błędów: {indexerNames}", + "IndexerTagSeriesHelpText": "Używaj tego indeksera tylko dla seriali z co najmniej jednym pasującym tagiem. Pozostaw puste, aby używać dla wszystkich seriali.", + "IndexerValidationCloudFlareCaptchaExpired": "Token CAPTCHA CloudFlare wygasł, odśwież go.", + "IndexerValidationCloudFlareCaptchaRequired": "Strona chroniona przez CAPTCHA CloudFlare. Wymagany prawidłowy token CAPTCHA.", + "IndexerValidationFeedNotSupported": "Kanał indeksera nie jest obsługiwany: {exceptionMessage}", + "IndexerValidationInvalidApiKey": "Nieprawidłowy klucz API", + "IndexerValidationJackettAllNotSupported": "Endpoint Jackett 'all' nie jest obsługiwany, dodaj indeksery pojedynczo", + "IndexerValidationJackettAllNotSupportedHelpText": "Endpoint Jackett 'all' nie jest obsługiwany, dodaj indeksery pojedynczo", + "IndexerValidationNoResultsInConfiguredCategories": "Zapytanie zakończone powodzeniem, ale indekser nie zwrócił wyników w skonfigurowanych kategoriach. Problem może dotyczyć indeksera lub ustawień kategorii indeksera.", + "IndexerValidationNoRssFeedQueryAvailable": "Brak dostępnego zapytania kanału RSS. Problem może dotyczyć indeksera lub ustawień kategorii indeksera.", + "IndexerValidationQuerySeasonEpisodesNotSupported": "Indekser nie obsługuje bieżącego zapytania. Sprawdź, czy kategorie oraz wyszukiwanie sezonów/odcinków są obsługiwane. Sprawdź log, aby uzyskać więcej szczegółów.", + "IndexerValidationRequestLimitReached": "Osiągnięto limit żądań: {exceptionMessage}", + "IndexerValidationSearchParametersNotSupported": "Indekser nie obsługuje wymaganych parametrów wyszukiwania", + "IndexerValidationTestAbortedDueToError": "Test został przerwany z powodu błędu: {exceptionMessage}", + "IndexerValidationUnableToConnect": "Nie można połączyć się z indekserem: {exceptionMessage}. Sprawdź log wokół tego błędu, aby uzyskać szczegóły", + "IndexerValidationUnableToConnectHttpError": "Nie można połączyć się z indekserem, sprawdź ustawienia DNS i upewnij się, że IPv6 działa albo jest wyłączone. {exceptionMessage}.", + "IndexerValidationUnableToConnectInvalidCredentials": "Nie można połączyć się z indekserem, nieprawidłowe dane logowania. {exceptionMessage}.", + "IndexerValidationUnableToConnectResolutionFailure": "Nie można połączyć się z indekserem z powodu błędu połączenia. Sprawdź połączenie z serwerem indeksera i DNS. {exceptionMessage}.", + "IndexerValidationUnableToConnectServerUnavailable": "Nie można połączyć się z indekserem, serwer indeksera jest niedostępny. Spróbuj ponownie później. {exceptionMessage}.", + "IndexerValidationUnableToConnectTimeout": "Nie można połączyć się z indekserem, możliwe że z powodu przekroczenia czasu. Spróbuj ponownie lub sprawdź ustawienia sieci. {exceptionMessage}.", + "Indexers": "Indeksery", + "IndexersLoadError": "Nie można wczytać indekserów", + "IndexersSettingsSummary": "Indeksery i opcje indekserów", + "Info": "Informacje", + "InfoUrl": "URL informacji", + "Install": "Zainstaluj", + "InstallLatest": "Zainstaluj najnowsze", + "InstallMajorVersionUpdate": "Zainstaluj aktualizację", + "InstallMajorVersionUpdateMessage": "Ta aktualizacja zainstaluje nową główną wersję i może nie być kompatybilna z Twoim systemem. Czy na pewno chcesz ją zainstalować?", + "InstallMajorVersionUpdateMessageLink": "Sprawdź [{domain}]({url}), aby uzyskać więcej informacji.", + "InstanceName": "Nazwa instancji", + "InstanceNameHelpText": "Nazwa instancji na karcie i jako nazwa aplikacji Syslog", + "InteractiveImport": "Import interaktywny", + "InteractiveImportLoadError": "Nie można wczytać elementów importu ręcznego", + "InteractiveImportMultipleQueueItems": "Wiele elementów kolejki", + "InteractiveImportNoEpisode": "Dla każdego wybranego pliku trzeba wybrać co najmniej jeden odcinek", + "InteractiveImportNoFilesFound": "W wybranym folderze nie znaleziono plików wideo", + "InteractiveImportNoImportMode": "Musisz wybrać tryb importu", + "InteractiveImportNoLanguage": "Dla każdego wybranego pliku trzeba wybrać język(i)", + "InteractiveImportNoQuality": "Dla każdego wybranego pliku trzeba wybrać jakość", + "InteractiveImportNoSeason": "Dla każdego wybranego pliku trzeba wybrać sezon", + "InteractiveImportNoSeries": "Dla każdego wybranego pliku trzeba wybrać serial", + "InteractiveSearch": "Wyszukiwanie interaktywne", + "InteractiveSearchGrabError": "Nie udało się dodać do kolejki pobierania", + "InteractiveSearchModalHeader": "Wyszukiwanie interaktywne", + "InteractiveSearchModalHeaderSeason": "Wyszukiwanie interaktywne - {season}", + "InteractiveSearchResultsSeriesFailedErrorMessage": "Wyszukiwanie nie powiodło się, ponieważ {message}. Spróbuj odświeżyć informacje o serialu i sprawdzić, czy wymagane dane są dostępne, zanim wyszukasz ponownie.", + "InteractiveSearchSeason": "Wyszukiwanie interaktywne wszystkich odcinków w tym sezonie", + "Interval": "Interwał", + "InvalidFormat": "Nieprawidłowy format", + "InvalidUILanguage": "Twój interfejs jest ustawiony na nieprawidłowy język, popraw to i zapisz ustawienia", + "KeepAndTagSeries": "Zachowaj i otaguj serial", + "KeepAndUnmonitorSeries": "Zachowaj i przestań monitorować serial", + "KeyboardShortcuts": "Skróty klawiaturowe", + "KeyboardShortcutsCloseModal": "Zamknij bieżące okno modalne", + "KeyboardShortcutsConfirmModal": "Potwierdź okno potwierdzenia", + "KeyboardShortcutsFocusSearchBox": "Ustaw fokus na polu wyszukiwania", + "KeyboardShortcutsOpenModal": "Otwórz to okno modalne", + "KeyboardShortcutsSaveSettings": "Zapisz ustawienia", + "Label": "Etykieta", + "LabelIsRequired": "Etykieta jest wymagana", + "Language": "Język", + "Languages": "Języki", + "LanguagesLoadError": "Nie można wczytać języków", + "Large": "Duży", + "LastDuration": "Ostatni czas trwania", + "LastExecution": "Ostatnie wykonanie", + "LastSearched": "Ostatnio wyszukiwano", + "LastUsed": "Ostatnio użyte", + "LastWriteTime": "Czas ostatniego zapisu", + "LatestSeason": "Najnowszy sezon", + "Level": "Poziom", + "LiberaWebchat": "Webchat Libera", + "LibraryImport": "Import biblioteki", + "LibraryImportSeriesHeader": "Importuj seriale, które już masz", + "LibraryImportTips": "Wskazówki, aby import przebiegł sprawnie:", + "LibraryImportTipsDontUseDownloadsFolder": "Nie używaj tego do importu pobrań z klienta pobierania, to jest tylko dla istniejących uporządkowanych bibliotek, nie dla nieposortowanych plików.", + "LibraryImportTipsQualityInEpisodeFilename": "Upewnij się, że nazwy plików zawierają jakość, np. `episode.s02e15.bluray.mkv`", + "LibraryImportTipsSeriesUseRootFolder": "Wskaż w {appName} folder zawierający wszystkie seriale TV, a nie pojedynczy serial, np. \"`{goodFolderExample}`\" a nie \"`{badFolderExample}`\". Dodatkowo każdy serial musi znajdować się w osobnym folderze w folderze głównym/biblioteki.", + "Links": "Linki", + "ListExclusionsLoadError": "Nie można wczytać wykluczeń list", + "ListOptionsLoadError": "Nie można wczytać opcji listy", + "ListQualityProfileHelpText": "Elementy listy będą dodawane z profilem jakości", + "ListRootFolderHelpText": "Elementy listy będą dodawane do folderu głównego", + "ListSyncLevelHelpText": "Seriale w bibliotece będą obsługiwane zgodnie z Twoim wyborem, jeśli wypadną z list(y) lub przestaną się na niej pojawiać", + "ListSyncTag": "Tag synchronizacji list", + "ListSyncTagHelpText": "Ten tag zostanie dodany, gdy serial wypadnie z list(y) lub już się na niej nie pojawia", + "ListTagsHelpText": "Tagi, które zostaną dodane przy imporcie z tej listy", + "ListWillRefreshEveryInterval": "Lista będzie odświeżana co {refreshInterval}", + "ListsLoadError": "Nie można wczytać list", + "Local": "Lokalny", + "LocalAirDate": "Lokalna data emisji", + "LocalPath": "Lokalna ścieżka", + "LocalStorageIsNotSupported": "Local Storage nie jest obsługiwany lub jest wyłączony. Wtyczka albo tryb prywatny mogły go wyłączyć.", + "Location": "Lokalizacja", + "LogFiles": "Pliki logów", + "LogFilesLocation": "Pliki logów znajdują się w: {location}", + "LogLevel": "Poziom logowania", + "LogLevelTraceHelpTextWarning": "Logowanie Trace powinno być włączane tylko tymczasowo", + "LogOnly": "Tylko loguj", + "LogSizeLimit": "Limit rozmiaru logu", + "LogSizeLimitHelpText": "Maksymalny rozmiar pliku logu w MB przed archiwizacją. Domyślnie 1MB.", + "Logging": "Logowanie", + "Logout": "Wyloguj", + "Logs": "Logi", + "LongDateFormat": "Długi format daty", + "Lowercase": "Małe litery", + "MainNavigation": "Główna nawigacja", + "MaintenanceRelease": "Wydanie konserwacyjne: poprawki błędów i inne usprawnienia. Zobacz historię commitów GitHub, aby uzyskać więcej szczegółów", + "ManageClients": "Zarządzaj klientami", + "ManageCustomFormats": "Zarządzaj formatami niestandardowymi", + "ManageDownloadClients": "Zarządzaj klientami pobierania", + "ManageEpisodes": "Zarządzaj odcinkami", + "ManageEpisodesSeason": "Zarządzaj plikami odcinków w tym sezonie", + "ManageFormats": "Zarządzaj formatami", + "ManageImportLists": "Zarządzaj listami importu", + "ManageIndexers": "Zarządzaj indekserami", + "ManageLists": "Zarządzaj listami", + "Manual": "Ręcznie", + "ManualGrab": "Ręczne pobranie", + "ManualImport": "Import ręczny", + "ManualImportItemsLoadError": "Nie można wczytać elementów importu ręcznego", + "MappedNetworkDrivesWindowsService": "Mapowane dyski sieciowe nie są dostępne przy uruchomieniu jako usługa Windows. Więcej informacji w [FAQ]({url}).", + "Mapping": "Mapowanie", + "MarkAsFailed": "Oznacz jako nieudane", + "MarkAsFailedConfirmation": "Czy na pewno chcesz oznaczyć '{sourceTitle}' jako nieudane?", + "MassSearchCancelWarning": "Po uruchomieniu nie można tego anulować bez restartu {appName} lub wyłączenia wszystkich indekserów.", + "MatchedToEpisodes": "Dopasowano do odcinków", + "MatchedToSeason": "Dopasowano do sezonu", + "MatchedToSeries": "Dopasowano do serialu", + "Maximum": "Maksimum", + "MaximumLimits": "Maksymalne limity", + "MaximumSingleEpisodeAge": "Maksymalny wiek pojedynczego odcinka", + "MaximumSingleEpisodeAgeHelpText": "Podczas pełnego wyszukiwania sezonu dozwolone będą tylko paczki sezonu, gdy ostatni odcinek sezonu jest starszy niż to ustawienie. Dotyczy tylko standardowych seriali. Ustaw 0, aby wyłączyć.", + "MaximumSize": "Maksymalny rozmiar", + "MaximumSizeHelpText": "Maksymalny rozmiar wydania do pobrania w MB. Ustaw zero, aby brak limitu", + "Mechanism": "Mechanizm", + "MediaInfo": "Informacje o mediach", + "MediaInfoAudioStreamHeader": "Strumień audio #{number}", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages obsługuje sufiks `:EN+DE`, który pozwala filtrować języki uwzględniane w nazwie pliku. Użyj `-DE`, aby wykluczyć określone języki. Dodanie `+` (np. `:EN+`) zwróci `[EN]`/`[EN+--]`/`[--]` zależnie od wykluczonych języków. Przykład: `{MediaInfo Full:EN+DE}`.", + "MediaInfoFootNote2": "MediaInfo AudioLanguages wyklucza angielski, jeśli jest jedynym językiem. Użyj MediaInfo AudioLanguagesAll, aby uwzględnić przypadki tylko z angielskim", + "MediaInfoForced": "Wymuszone", + "MediaInfoHearingImpaired": "Dla niesłyszących", + "MediaInfoSubtitlesHeader": "Napisy", + "MediaManagement": "Zarządzanie mediami", + "MediaManagementSettings": "Ustawienia zarządzania mediami", + "MediaManagementSettingsLoadError": "Nie można wczytać ustawień zarządzania mediami", + "MediaManagementSettingsSummary": "Nazewnictwo, ustawienia zarządzania plikami i foldery główne", + "Medium": "Średni", + "Menu": "Menu", + "Message": "Wiadomość", + "Metadata": "Metadane", + "MetadataKometaDeprecated": "Pliki Kometa nie będą już tworzone, obsługa zostanie całkowicie usunięta w v5", + "MetadataKometaDeprecatedSetting": "Przestarzałe", + "MetadataLoadError": "Nie można wczytać metadanych", + "MetadataPlexSettingsEpisodeMappings": "Mapowania odcinków", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Uwzględnij mapowania odcinków dla wszystkich plików w pliku .plexmatch", + "MetadataPlexSettingsSeriesPlexMatchFile": "Plik Plex Match serialu", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Tworzy plik .plexmatch w folderze serialu", + "MetadataProvidedBy": "Metadane są dostarczane przez {provider}", + "MetadataSettings": "Ustawienia metadanych", + "MetadataSettingsEpisodeImages": "Obrazy odcinków", + "MetadataSettingsEpisodeMetadata": "Metadane odcinków", + "MetadataSettingsEpisodeMetadataImageThumbs": "Miniatury obrazów metadanych odcinków", + "MetadataSettingsSeasonImages": "Obrazy sezonów", + "MetadataSettingsSeriesImages": "Obrazy serialu", + "MetadataSettingsSeriesMetadata": "Metadane serialu", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Przewodnik odcinków w metadanych serialu", + "MetadataSettingsSeriesMetadataUrl": "URL metadanych serialu", + "MetadataSettingsSeriesSummary": "Twórz pliki metadanych przy imporcie odcinków lub odświeżaniu serialu", + "MetadataSource": "Źródło metadanych", + "MetadataSourceSettings": "Ustawienia źródła metadanych", + "MetadataSourceSettingsSeriesSummary": "Informacje o tym, skąd {appName} pobiera informacje o serialach i odcinkach", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Uwzględnij tagi miniatur obrazów w .nfo (wymaga 'Episode Metadata')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Uwzględnij element przewodnika odcinków w formacie JSON w tvshow.nfo (wymaga 'Series Metadata')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo z pełnymi metadanymi serialu", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Uwzględnij URL serialu TheTVDB w tvshow.nfo (można łączyć z 'Series Metadata')", + "MidseasonFinale": "Finał śródsezonowy", + "Minimum": "Minimum", + "MinimumAge": "Minimalny wiek", + "MinimumAgeHelpText": "Tylko Usenet: minimalny wiek NZB w minutach przed pobraniem. Użyj tego, aby dać nowym wydaniom czas na propagację do dostawcy Usenet.", + "MinimumCustomFormatScore": "Minimalny wynik formatu niestandardowego", + "MinimumCustomFormatScoreHelpText": "Minimalny dozwolony wynik formatu niestandardowego do pobrania", + "MinimumCustomFormatScoreIncrement": "Minimalny przyrost wyniku formatu niestandardowego", + "MinimumCustomFormatScoreIncrementHelpText": "Minimalna wymagana poprawa wyniku formatu niestandardowego między istniejącymi i nowymi wydaniami, zanim {appName} uzna to za ulepszenie", + "MinimumFreeSpace": "Minimalna wolna przestrzeń", + "MinimumFreeSpaceHelpText": "Zablokuj import, jeśli po nim wolna przestrzeń będzie mniejsza niż ta wartość", + "MinimumLimits": "Minimalne limity", + "Minute": "minuta", + "MinuteShorthand": "m", + "MinutesFortyFive": "45 minut: {fortyFive}", + "MinutesSixty": "60 minut: {sixty}", + "MinutesThirty": "30 minut: {thirty}", + "Missing": "Brakujące", + "MissingEpisodes": "Brakujące odcinki", + "MissingLoadError": "Błąd ładowania brakujących elementów", + "MissingNoItems": "Brak brakujących elementów", + "Mixed": "Mieszane", + "Mode": "Tryb", + "Monday": "Poniedziałek", + "Monitor": "Monitoruj", + "MonitorAllEpisodes": "Wszystkie odcinki", + "MonitorAllEpisodesDescription": "Monitoruj wszystkie odcinki poza specjalnymi", + "MonitorAllSeasons": "Wszystkie sezony", + "MonitorAllSeasonsDescription": "Automatycznie monitoruj wszystkie nowe sezony", + "MonitorEpisodes": "Monitoruj odcinki", + "MonitorEpisodesModalInfo": "To ustawienie zmienia tylko to, które odcinki lub sezony są monitorowane w obrębie serialu. Wybranie opcji 'Brak' wyłączy monitorowanie serialu", + "MonitorExistingEpisodes": "Istniejące odcinki", + "MonitorExistingEpisodesDescription": "Monitoruj odcinki, które mają pliki lub nie zostały jeszcze wyemitowane", + "MonitorFirstSeason": "Pierwszy sezon", + "MonitorFirstSeasonDescription": "Monitoruj wszystkie odcinki pierwszego sezonu. Pozostałe sezony będą ignorowane", + "MonitorFutureEpisodes": "Przyszłe odcinki", + "MonitorFutureEpisodesDescription": "Monitoruj odcinki, które nie zostały jeszcze wyemitowane", + "MonitorLastSeason": "Ostatni sezon", + "MonitorLastSeasonDescription": "Monitoruj wszystkie odcinki ostatniego sezonu", + "MonitorMissingEpisodes": "Brakujące odcinki", + "MonitorMissingEpisodesDescription": "Monitoruj odcinki, które nie mają plików lub nie zostały jeszcze wyemitowane", + "MonitorNewItems": "Monitoruj nowe elementy", + "MonitorNewSeasons": "Monitoruj nowe sezony", + "MonitorNewSeasonsHelpText": "Które nowe sezony mają być monitorowane automatycznie", + "MonitorNoEpisodes": "Brak", + "MonitorNoEpisodesDescription": "Żadne odcinki nie będą monitorowane", + "MonitorNoNewSeasons": "Bez nowych sezonów", + "MonitorNoNewSeasonsDescription": "Nie monitoruj automatycznie żadnych nowych sezonów", + "MonitorPilotEpisode": "Odcinek pilotowy", + "MonitorPilotEpisodeDescription": "Monitoruj tylko pierwszy odcinek pierwszego sezonu", + "MonitorRecentEpisodes": "Najnowsze odcinki", + "MonitorRecentEpisodesDescription": "Monitoruj odcinki wyemitowane w ciągu ostatnich 90 dni oraz przyszłe odcinki", + "MonitorSelected": "Monitoruj zaznaczone", + "MonitorSeries": "Monitoruj serial", + "MonitorSpecialEpisodes": "Monitoruj odcinki specjalne", + "MonitorSpecialEpisodesDescription": "Monitoruj wszystkie odcinki specjalne bez zmiany statusu monitorowania pozostałych odcinków", + "Monitored": "Monitorowane", + "MonitoredAll": "Wszystkie", + "MonitoredEpisodesHelpText": "Pobieraj monitorowane odcinki tego serialu", + "MonitoredNone": "Brak", + "MonitoredOnly": "Tylko monitorowane", + "MonitoredPartial": "Częściowo", + "MonitoredStatus": "Monitorowane/Status", + "Monitoring": "Monitorowanie", + "MonitoringOptions": "Opcje monitorowania", + "Month": "Miesiąc", + "More": "Więcej", + "MoreDetails": "Więcej szczegółów", + "MoreInfo": "Więcej informacji", + "MountSeriesHealthCheckMessage": "Punkt montowania zawierający ścieżkę serialu jest zamontowany tylko do odczytu: ", + "MoveAutomatically": "Przenoś automatycznie", + "MoveFiles": "Przenieś pliki", + "MoveSeriesFoldersDontMoveFiles": "Nie, sam przeniosę pliki", + "MoveSeriesFoldersMoveFiles": "Tak, przenieś pliki", + "MoveSeriesFoldersToNewPath": "Czy chcesz przenieść pliki serialu z '{originalPath}' do '{destinationPath}'?", + "MoveSeriesFoldersToRootFolder": "Czy chcesz przenieść foldery seriali do '{destinationRootFolder}'?", + "MultiEpisode": "Wiele odcinków", + "MultiEpisodeInvalidFormat": "Wiele odcinków: nieprawidłowy format", + "MultiEpisodeStyle": "Styl wielu odcinków", + "MultiLanguages": "Wiele języków", + "MultiSeason": "Wiele sezonów", + "MultipleEpisodes": "Wiele odcinków", + "MustContain": "Musi zawierać", + "MustContainHelpText": "Wydanie musi zawierać co najmniej jeden z tych terminów (bez rozróżniania wielkości liter)", + "MustNotContain": "Nie może zawierać", + "MustNotContainHelpText": "Wydanie zostanie odrzucone, jeśli zawiera jeden lub więcej z terminów (bez rozróżniania wielkości liter)", + "MyComputer": "Mój komputer", + "Name": "Nazwa", + "NamingSettings": "Ustawienia nazewnictwa", + "NamingSettingsLoadError": "Nie można wczytać ustawień nazewnictwa", + "Negate": "Neguj", + "NegateHelpText": "Jeśli zaznaczone, format niestandardowy nie zostanie zastosowany, jeśli ten warunek {implementationName} pasuje.", + "Negated": "Zanegowane", + "Network": "Sieć", + "Never": "Nigdy", + "New": "Nowe", + "NextAiring": "Następna emisja", + "NextAiringDate": "Następna emisja: {date}", + "NextExecution": "Następne wykonanie", + "No": "Nie", + "NoBackupsAreAvailable": "Brak dostępnych kopii zapasowych", + "NoBlocklistItems": "Brak elementów na czarnej liście", + "NoChange": "Bez zmian", + "NoChanges": "Brak zmian", + "NoCustomFormatsFound": "Nie znaleziono formatów niestandardowych", + "NoDelay": "Brak opóźnienia", + "NoDownloadClientsFound": "Nie znaleziono klientów pobierania", + "NoEpisodeHistory": "Brak historii odcinka", + "NoEpisodeInformation": "Brak dostępnych informacji o odcinku.", + "NoEpisodeOverview": "Brak opisu odcinka", + "NoEpisodesFoundForSelectedSeason": "Nie znaleziono odcinków dla wybranego sezonu", + "NoEpisodesInThisSeason": "Brak odcinków w tym sezonie", + "NoEventsFound": "Nie znaleziono zdarzeń", + "NoHistory": "Brak historii", + "NoHistoryFound": "Nie znaleziono historii", + "NoImportListsFound": "Nie znaleziono list importu", + "NoIndexersFound": "Nie znaleziono indekserów", + "NoIssuesWithYourConfiguration": "Brak problemów z konfiguracją", + "NoLeaveIt": "Nie, zostaw", + "NoLimitForAnyRuntime": "Brak limitu dla dowolnego czasu trwania", + "NoLinks": "Brak linków", + "NoLogFiles": "Brak plików logów", + "NoMatchFound": "Nie znaleziono dopasowania!", + "NoMinimumForAnyRuntime": "Brak minimum dla dowolnego czasu trwania", + "NoMonitoredEpisodes": "Brak monitorowanych odcinków w tym serialu", + "NoMonitoredEpisodesSeason": "Brak monitorowanych odcinków w tym sezonie", + "NoResultsFound": "Nie znaleziono wyników", + "NoSeasons": "Brak sezonów", + "NoSeriesFoundImportOrAdd": "Nie znaleziono seriali. Na początek zaimportuj istniejące seriale albo dodaj nowy serial.", + "NoSeriesHaveBeenAdded": "Nie dodano jeszcze żadnych seriali. Chcesz najpierw zaimportować część lub wszystkie seriale?", + "NoTagsHaveBeenAddedYet": "Nie dodano jeszcze żadnych tagów", + "NoUpdatesAreAvailable": "Brak dostępnych aktualizacji", + "None": "Brak", + "NotSeasonPack": "To nie jest paczka sezonu", + "NotificationStatusAllClientHealthCheckMessage": "Wszystkie powiadomienia są niedostępne z powodu błędów", + "NotificationStatusSingleClientHealthCheckMessage": "Powiadomienia niedostępne z powodu błędów: {notificationNames}", + "NotificationTriggers": "Wyzwalacze powiadomień", + "NotificationTriggersHelpText": "Wybierz, które zdarzenia mają wyzwalać to powiadomienie", + "NotificationsAppriseSettingsConfigurationKey": "Klucz konfiguracji Apprise", + "NotificationsAppriseSettingsConfigurationKeyHelpText": "Klucz konfiguracji dla rozwiązania trwałego przechowywania. Pozostaw puste, jeśli używane są bezstanowe URL-e.", + "NotificationsAppriseSettingsIncludePoster": "Uwzględnij plakat", + "NotificationsAppriseSettingsIncludePosterHelpText": "Uwzględnij plakat w wiadomości", + "NotificationsAppriseSettingsNotificationType": "Typ powiadomienia Apprise", + "NotificationsAppriseSettingsPasswordHelpText": "Hasło HTTP Basic Auth", + "NotificationsAppriseSettingsServerUrl": "URL serwera Apprise", + "NotificationsAppriseSettingsServerUrlHelpText": "URL serwera Apprise, w tym http(s):// i port, jeśli potrzebny", + "NotificationsAppriseSettingsStatelessUrls": "Bezstanowe URL-e Apprise", + "NotificationsAppriseSettingsStatelessUrlsHelpText": "Jeden lub więcej URL-i oddzielonych przecinkami, określających dokąd wysłać powiadomienie. Pozostaw puste, jeśli używane jest trwałe przechowywanie.", + "NotificationsAppriseSettingsTags": "Tagi Apprise", + "NotificationsAppriseSettingsTagsHelpText": "Opcjonalnie powiadamiaj tylko elementy z odpowiednimi tagami.", + "NotificationsAppriseSettingsUsernameHelpText": "Nazwa użytkownika HTTP Basic Auth", + "NotificationsCustomScriptSettingsArguments": "Argumenty", + "NotificationsCustomScriptSettingsArgumentsHelpText": "Argumenty przekazywane do skryptu", + "NotificationsCustomScriptSettingsName": "Skrypt niestandardowy", + "NotificationsCustomScriptSettingsProviderMessage": "Test uruchomi skrypt z typem zdarzenia ustawionym na {eventTypeTest}. Upewnij się, że Twój skrypt obsługuje to poprawnie", + "NotificationsCustomScriptValidationFileDoesNotExist": "Plik nie istnieje", + "NotificationsDiscordSettingsAuthor": "Autor", + "NotificationsDiscordSettingsAuthorHelpText": "Nadpisz autora embeda wyświetlanego dla tego powiadomienia. Puste oznacza nazwę instancji", + "NotificationsDiscordSettingsAvatar": "Awatar", + "NotificationsDiscordSettingsAvatarHelpText": "Zmień awatar używany dla wiadomości z tej integracji", + "NotificationsDiscordSettingsOnGrabFields": "Pola dla zdarzenia przy pobraniu", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Zmień pola przekazywane dla powiadomienia 'przy pobraniu'", + "NotificationsDiscordSettingsOnImportFields": "Pola dla zdarzenia przy imporcie", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Zmień pola przekazywane dla powiadomienia 'przy imporcie'", + "NotificationsDiscordSettingsOnManualInteractionFields": "Pola dla zdarzenia przy interakcji ręcznej", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Zmień pola przekazywane dla powiadomienia 'przy interakcji ręcznej'", + "NotificationsDiscordSettingsUsernameHelpText": "Nazwa użytkownika, pod którą publikować; domyślnie używana jest domyślna nazwa webhooka Discord", + "NotificationsDiscordSettingsWebhookUrlHelpText": "URL webhooka kanału Discord", + "NotificationsEmailSettingsBccAddress": "Adres(y) BCC", + "NotificationsEmailSettingsBccAddressHelpText": "Lista adresów BCC oddzielonych przecinkami", + "NotificationsEmailSettingsCcAddress": "Adres(y) CC", + "NotificationsEmailSettingsCcAddressHelpText": "Lista adresów CC oddzielonych przecinkami", + "NotificationsEmailSettingsFromAddress": "Adres nadawcy", + "NotificationsEmailSettingsName": "Email", + "NotificationsEmailSettingsRecipientAddress": "Adres(y) odbiorców", + "NotificationsEmailSettingsRecipientAddressHelpText": "Lista adresów odbiorców email oddzielonych przecinkami", + "NotificationsEmailSettingsServer": "Serwer", + "NotificationsEmailSettingsServerHelpText": "Nazwa hosta lub IP serwera email", + "NotificationsEmailSettingsUseEncryption": "Użyj szyfrowania", + "NotificationsEmailSettingsUseEncryptionHelpText": "Czy preferować szyfrowanie, jeśli serwer je obsługuje, zawsze używać szyfrowania przez SSL (tylko port 465) lub StartTLS (dowolny inny port), czy nigdy nie używać szyfrowania", + "NotificationsEmbySettingsSendNotifications": "Wysyłaj powiadomienia", + "NotificationsEmbySettingsSendNotificationsHelpText": "Pozwól Emby wysyłać powiadomienia do skonfigurowanych dostawców. Nieobsługiwane w Jellyfin.", + "NotificationsEmbySettingsUpdateLibraryHelpText": "Aktualizuj bibliotekę przy imporcie, zmianie nazwy lub usunięciu", + "NotificationsGotifySettingIncludeSeriesPoster": "Uwzględnij plakat serialu", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Uwzględnij plakat serialu w wiadomości", + "NotificationsGotifySettingsAppToken": "Token aplikacji", + "NotificationsGotifySettingsAppTokenHelpText": "Token aplikacji wygenerowany przez Gotify", + "NotificationsGotifySettingsMetadataLinks": "Linki metadanych", + "NotificationsGotifySettingsMetadataLinksHelpText": "Dodawaj linki do metadanych serialu przy wysyłaniu powiadomień", + "NotificationsGotifySettingsPreferredMetadataLink": "Preferowany link metadanych", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Link metadanych dla klientów obsługujących tylko jeden link", + "NotificationsGotifySettingsPriorityHelpText": "Priorytet powiadomienia", + "NotificationsGotifySettingsServer": "Serwer Gotify", + "NotificationsGotifySettingsServerHelpText": "URL serwera Gotify, w tym http(s):// i port, jeśli potrzebny", + "NotificationsJoinSettingsApiKeyHelpText": "Klucz API z ustawień konta Join (kliknij przycisk Join API).", + "NotificationsJoinSettingsDeviceIds": "ID urządzeń", + "NotificationsJoinSettingsDeviceIdsHelpText": "Przestarzałe, użyj nazw urządzeń. Lista ID urządzeń oddzielona przecinkami, do których chcesz wysyłać powiadomienia. Jeśli puste, powiadomienia trafią do wszystkich urządzeń.", + "NotificationsJoinSettingsDeviceNames": "Nazwy urządzeń", + "NotificationsJoinSettingsDeviceNamesHelpText": "Lista pełnych lub częściowych nazw urządzeń oddzielona przecinkami, do których chcesz wysyłać powiadomienia. Jeśli puste, powiadomienia trafią do wszystkich urządzeń.", + "NotificationsJoinSettingsNotificationPriority": "Priorytet powiadomienia", + "NotificationsJoinValidationInvalidDeviceId": "ID urządzeń wydają się nieprawidłowe.", + "NotificationsKodiSettingAlwaysUpdate": "Zawsze aktualizuj", + "NotificationsKodiSettingAlwaysUpdateHelpText": "Aktualizować bibliotekę nawet gdy odtwarzane jest wideo?", + "NotificationsKodiSettingsCleanLibrary": "Wyczyść bibliotekę", + "NotificationsKodiSettingsCleanLibraryHelpText": "Czyść bibliotekę po aktualizacji", + "NotificationsKodiSettingsDisplayTime": "Czas wyświetlania", + "NotificationsKodiSettingsDisplayTimeHelpText": "Jak długo powiadomienie ma być wyświetlane (w sekundach)", + "NotificationsKodiSettingsGuiNotification": "Powiadomienie GUI", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Aktualizować bibliotekę przy imporcie i zmianie nazwy?", + "NotificationsMailgunSettingsApiKeyHelpText": "Klucz API wygenerowany przez MailGun", + "NotificationsMailgunSettingsSenderDomain": "Domena nadawcy", + "NotificationsMailgunSettingsUseEuEndpoint": "Użyj endpointu UE", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Włącz, aby używać endpointu UE MailGun", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Twój klucz API z profilu", + "NotificationsNtfySettingsAccessToken": "Token dostępu", + "NotificationsNtfySettingsAccessTokenHelpText": "Opcjonalne uwierzytelnianie tokenem. Ma priorytet nad nazwą użytkownika/hasłem", + "NotificationsNtfySettingsClickUrl": "URL kliknięcia", + "NotificationsNtfySettingsClickUrlHelpText": "Opcjonalny link po kliknięciu powiadomienia", + "NotificationsNtfySettingsPasswordHelpText": "Opcjonalne hasło", + "NotificationsNtfySettingsServerUrl": "URL serwera", + "NotificationsNtfySettingsServerUrlHelpText": "Pozostaw puste, aby użyć publicznego serwera ({url})", + "NotificationsNtfySettingsTagsEmojis": "Tagi i emoji Ntfy", + "NotificationsNtfySettingsTagsEmojisHelpText": "Opcjonalna lista tagów lub emoji do użycia", + "NotificationsNtfySettingsTopics": "Tematy", + "NotificationsNtfySettingsTopicsHelpText": "Lista tematów, do których wysyłać powiadomienia", + "NotificationsNtfySettingsUsernameHelpText": "Opcjonalna nazwa użytkownika", + "NotificationsNtfyValidationAuthorizationRequired": "Wymagana autoryzacja", + "NotificationsPlexSettingsAuthToken": "Token uwierzytelniania", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Uwierzytelnij przez Plex.tv", + "NotificationsPlexSettingsServer": "Serwer", + "NotificationsPlexSettingsServerHelpText": "Wybierz serwer z konta plex.tv po uwierzytelnieniu", + "NotificationsPlexValidationNoTvLibraryFound": "Wymagana jest co najmniej jedna biblioteka TV", + "NotificationsPushBulletSettingSenderId": "ID nadawcy", + "NotificationsPushBulletSettingSenderIdHelpText": "ID urządzenia, z którego wysyłać powiadomienia; użyj device_iden z URL urządzenia na pushbullet.com (pozostaw puste, aby wysyłać od siebie)", + "NotificationsPushBulletSettingsAccessToken": "Token dostępu", + "NotificationsPushBulletSettingsChannelTags": "Tagi kanałów", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Lista tagów kanałów, do których wysyłać powiadomienia", + "NotificationsPushBulletSettingsDeviceIds": "ID urządzeń", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista ID urządzeń (pozostaw puste, aby wysyłać do wszystkich urządzeń)", + "NotificationsPushcutSettingsApiKeyHelpText": "Kluczami API można zarządzać w widoku Account aplikacji Pushcut", + "NotificationsPushcutSettingsIncludePoster": "Uwzględnij plakat", + "NotificationsPushcutSettingsIncludePosterHelpText": "Uwzględnij plakat w powiadomieniu", + "NotificationsPushcutSettingsMetadataLinks": "Linki metadanych", + "NotificationsPushcutSettingsMetadataLinksHelpText": "Dodawaj linki do metadanych serialu przy wysyłaniu powiadomień", + "NotificationsPushcutSettingsNotificationName": "Nazwa powiadomienia", + "NotificationsPushcutSettingsNotificationNameHelpText": "Nazwa powiadomienia z zakładki Notifications aplikacji Pushcut", + "NotificationsPushcutSettingsTimeSensitive": "Wrażliwe czasowo", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Włącz, aby oznaczyć powiadomienie jako \"Time Sensitive\"", + "NotificationsPushoverSettingsDevices": "Urządzenia", + "NotificationsPushoverSettingsDevicesHelpText": "Lista nazw urządzeń (pozostaw puste, aby wysyłać do wszystkich urządzeń)", + "NotificationsPushoverSettingsExpire": "Wygaśnięcie", + "NotificationsPushoverSettingsExpireHelpText": "Maksymalny czas ponawiania alertów Emergency, maksymalnie 86400 sekund", + "NotificationsPushoverSettingsRetry": "Ponowienie", + "NotificationsPushoverSettingsRetryHelpText": "Interwał ponawiania alertów Emergency, minimum 30 sekund", + "NotificationsPushoverSettingsSound": "Dźwięk", + "NotificationsPushoverSettingsSoundHelpText": "Dźwięk powiadomienia, pozostaw puste aby użyć domyślnego", + "NotificationsPushoverSettingsTtl": "Czas życia", + "NotificationsPushoverSettingsTtlHelpText": "Czas w sekundach przed wygaśnięciem wiadomości. Ustaw 0 dla czasu nieograniczonego", + "NotificationsPushoverSettingsUserKey": "Klucz użytkownika", + "NotificationsSendGridSettingsApiKeyHelpText": "Klucz API wygenerowany przez SendGrid", + "NotificationsSettingsUpdateLibrary": "Aktualizuj bibliotekę", + "NotificationsSettingsUpdateMapPathsFrom": "Mapuj ścieżki z", + "NotificationsSettingsUpdateMapPathsFromSeriesHelpText": "Ścieżka {appName}, używana do modyfikacji ścieżek seriali, gdy {serviceName} widzi lokalizację biblioteki inaczej niż {appName} (wymaga 'Update Library')", + "NotificationsSettingsUpdateMapPathsTo": "Mapuj ścieżki do", + "NotificationsSettingsUpdateMapPathsToSeriesHelpText": "Ścieżka {serviceName}, używana do modyfikacji ścieżek seriali, gdy {serviceName} widzi lokalizację biblioteki inaczej niż {appName} (wymaga 'Update Library')", + "NotificationsSettingsUseSslHelpText": "Łącz z {serviceName} przez HTTPS zamiast HTTP", + "NotificationsSettingsWebhookHeaders": "Nagłówki", + "NotificationsSettingsWebhookMethod": "Metoda", + "NotificationsSettingsWebhookMethodHelpText": "Której metody HTTP użyć do wysłania danych do usługi web", + "NotificationsSettingsWebhookUrl": "URL webhooka", + "NotificationsSignalSettingsGroupIdPhoneNumber": "ID grupy / numer telefonu", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID grupy / numer telefonu odbiorcy", + "NotificationsSignalSettingsPasswordHelpText": "Hasło używane do uwierzytelniania żądań do signal-api", + "NotificationsSignalSettingsSenderNumber": "Numer nadawcy", + "NotificationsSignalSettingsSenderNumberHelpText": "Numer telefonu nadawcy zarejestrowany w signal-api", + "NotificationsSignalSettingsUsernameHelpText": "Nazwa użytkownika używana do uwierzytelniania żądań do signal-api", + "NotificationsSignalValidationSslRequired": "Wygląda na to, że SSL jest wymagany", + "NotificationsSimplepushSettingsEvent": "Zdarzenie", + "NotificationsSimplepushSettingsEventHelpText": "Dostosuj działanie powiadomień push", + "NotificationsSimplepushSettingsKey": "Klucz", + "NotificationsSlackSettingsChannel": "Kanał", + "NotificationsSlackSettingsChannelHelpText": "Nadpisuje domyślny kanał dla przychodzącego webhooka (#inny-kanal)", + "NotificationsSlackSettingsIcon": "Ikona", + "NotificationsSlackSettingsIconHelpText": "Zmień ikonę używaną w wiadomościach wysyłanych do Slacka (Emoji lub URL)", + "NotificationsSlackSettingsUsernameHelpText": "Nazwa użytkownika, jako który publikować w Slacku", + "NotificationsSlackSettingsWebhookUrlHelpText": "URL webhooka kanału Slack", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Wywołaj synoindex na localhost, aby zaktualizować plik biblioteki", + "NotificationsSynologyValidationInvalidOs": "Musi to być Synology", + "NotificationsSynologyValidationTestFailed": "To nie Synology albo synoindex jest niedostępny", + "NotificationsTagsSeriesHelpText": "Wysyłaj powiadomienia tylko dla seriali z co najmniej jednym pasującym tagiem", + "NotificationsTelegramSettingsBotToken": "Token bota", + "NotificationsTelegramSettingsChatId": "ID czatu", + "NotificationsTelegramSettingsChatIdHelpText": "Aby odbierać wiadomości, musisz rozpocząć rozmowę z botem lub dodać go do grupy", + "NotificationsTelegramSettingsIncludeAppName": "Uwzględnij {appName} w tytule", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcjonalnie dodaj przedrostek {appName} do tytułu wiadomości, aby odróżnić powiadomienia z różnych aplikacji", + "NotificationsTelegramSettingsIncludeInstanceName": "Uwzględnij nazwę instancji w tytule", + "NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Opcjonalnie uwzględnij nazwę instancji w powiadomieniu", + "NotificationsTelegramSettingsLinkPreview": "Podgląd linku", + "NotificationsTelegramSettingsLinkPreviewHelpText": "Określa, który link będzie podglądany w powiadomieniu Telegram. Wybierz 'None', aby wyłączyć", + "NotificationsTelegramSettingsMetadataLinks": "Linki metadanych", + "NotificationsTelegramSettingsMetadataLinksHelpText": "Dodaj linki do metadanych serialu przy wysyłaniu powiadomień", + "NotificationsTelegramSettingsSendSilently": "Wysyłaj po cichu", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Wysyła wiadomość bez dźwięku. Użytkownicy otrzymają powiadomienie bez dźwięku", + "NotificationsTelegramSettingsTopicId": "ID tematu", + "NotificationsTelegramSettingsTopicIdHelpText": "Podaj ID tematu, aby wysyłać powiadomienia do tego tematu. Pozostaw puste, aby użyć tematu ogólnego (tylko Supergrupy)", + "NotificationsTraktSettingsAccessToken": "Token dostępu", + "NotificationsTraktSettingsAuthUser": "Użytkownik auth", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Uwierzytelnij przez Trakt", + "NotificationsTraktSettingsExpires": "Wygasa", + "NotificationsTraktSettingsRefreshToken": "Token odświeżania", + "NotificationsTwitterSettingsAccessToken": "Token dostępu", + "NotificationsTwitterSettingsAccessTokenSecret": "Sekret tokenu dostępu", + "NotificationsTwitterSettingsConnectToTwitter": "Połącz z Twitter / X", + "NotificationsTwitterSettingsConsumerKey": "Klucz konsumenta", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Klucz konsumenta z aplikacji Twitter", + "NotificationsTwitterSettingsConsumerSecret": "Sekret konsumenta", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Sekret konsumenta z aplikacji Twitter", + "NotificationsTwitterSettingsDirectMessage": "Wiadomość prywatna", + "NotificationsTwitterSettingsDirectMessageHelpText": "Wyślij wiadomość prywatną zamiast publicznej", + "NotificationsTwitterSettingsMention": "Wzmianka", + "NotificationsTwitterSettingsMentionHelpText": "Wspomnij tego użytkownika w wysłanych tweetach", + "NotificationsValidationInvalidAccessToken": "Token dostępu jest nieprawidłowy", + "NotificationsValidationInvalidApiKey": "Klucz API jest nieprawidłowy", + "NotificationsValidationInvalidApiKeyExceptionMessage": "Klucz API jest nieprawidłowy: {exceptionMessage}", + "NotificationsValidationInvalidAuthenticationToken": "Token uwierzytelniania jest nieprawidłowy", + "NotificationsValidationInvalidHttpCredentials": "Dane logowania HTTP Auth są nieprawidłowe: {exceptionMessage}", + "NotificationsValidationInvalidUsernamePassword": "Nieprawidłowa nazwa użytkownika lub hasło", + "NotificationsValidationUnableToConnect": "Nie można połączyć się: {exceptionMessage}", + "NotificationsValidationUnableToConnectToApi": "Nie można połączyć się z API {service}. Połączenie z serwerem nie powiodło się: ({responseCode}) {exceptionMessage}", + "NotificationsValidationUnableToConnectToService": "Nie można połączyć się z {serviceName}", + "NotificationsValidationUnableToSendTestMessage": "Nie można wysłać wiadomości testowej: {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "Nie można wysłać wiadomości testowej. Odpowiedź API: {error}", + "NzbgetHistoryItemMessage": "Status PAR: {parStatus} - Status rozpakowania: {unpackStatus} - Status przenoszenia: {moveStatus} - Status skryptu: {scriptStatus} - Status usunięcia: {deleteStatus} - Status oznaczenia: {markStatus}", + "Ok": "Ok", + "OnApplicationUpdate": "Przy aktualizacji aplikacji", + "OnEpisodeFileDelete": "Przy usunięciu pliku odcinka", + "OnEpisodeFileDeleteForUpgrade": "Przy usunięciu pliku odcinka pod aktualizację", + "OnFileImport": "Przy imporcie pliku", + "OnFileUpgrade": "Przy aktualizacji pliku", + "OnGrab": "Przy pobraniu", + "OnHealthIssue": "Przy problemie stanu", + "OnHealthRestored": "Przy przywróceniu stanu", + "OnImportComplete": "Po zakończeniu importu", + "OnLatestVersion": "Najnowsza wersja {appName} jest już zainstalowana", + "OnManualInteractionRequired": "Przy wymaganej interakcji ręcznej", + "OnRename": "Przy zmianie nazwy", + "OnSeriesAdd": "Przy dodaniu serialu", + "OnSeriesDelete": "Przy usunięciu serialu", + "OneMinute": "1 minuta", + "OneSeason": "1 sezon", + "OnlyForBulkSeasonReleases": "Tylko dla zbiorczych wydań sezonu", + "OnlyTorrent": "Tylko torrent", + "OnlyUsenet": "Tylko Usenet", + "OpenBrowserOnStart": "Otwórz przeglądarkę przy starcie", + "OpenBrowserOnStartHelpText": " Otwórz przeglądarkę internetową i przejdź do strony głównej {appName} przy uruchomieniu aplikacji.", + "OpenSeries": "Otwórz serial", + "OptionalName": "Nazwa opcjonalna", + "Options": "Opcje", + "Or": "lub", + "Organize": "Organizuj", + "OrganizeLoadError": "Błąd ładowania podglądu", + "OrganizeModalHeader": "Organizuj i zmień nazwy", + "OrganizeModalHeaderSeason": "Organizuj i zmień nazwy - {season}", + "OrganizeNamingPattern": "Wzorzec nazwy: `{episodeFormat}`", + "OrganizeNothingToRename": "Sukces! Praca zakończona, brak plików do zmiany nazwy.", + "OrganizeRelativePaths": "Wszystkie ścieżki są względne do: `{path}`", + "OrganizeRenamingDisabled": "Zmiana nazw jest wyłączona, brak plików do zmiany nazwy", + "OrganizeSelectedSeriesModalAlert": "Wskazówka: aby podejrzeć zmianę nazwy, wybierz \"Anuluj\", następnie wybierz dowolny tytuł serialu i użyj tej ikony:", + "OrganizeSelectedSeriesModalConfirmation": "Czy na pewno chcesz uporządkować wszystkie pliki w {count} zaznaczonych serialach?", + "OrganizeSelectedSeriesModalHeader": "Organizuj zaznaczone seriale", + "Original": "Oryginalny", + "OriginalCountry": "Kraj oryginału", + "OriginalLanguage": "Język oryginału", + "Other": "Inne", + "OutputPath": "Ścieżka wyjściowa", + "OverrideAndAddToDownloadQueue": "Nadpisz i dodaj do kolejki pobierania", + "OverrideGrabModalTitle": "Nadpisz i pobierz - {title}", + "OverrideGrabNoEpisode": "Musi być wybrany co najmniej jeden odcinek", + "OverrideGrabNoLanguage": "Musi być wybrany co najmniej jeden język", + "OverrideGrabNoQuality": "Musi być wybrana jakość", + "OverrideGrabNoSeries": "Musi być wybrany serial", + "Overview": "Przegląd", + "OverviewOptions": "Opcje przeglądu", + "PackageVersion": "Wersja pakietu", + "PackageVersionInfo": "{packageVersion} autorstwa {packageAuthor}", + "PagerGoToFirstPage": "Idź do pierwszej strony", + "PagerGoToLastPage": "Idź do ostatniej strony", + "PagerGoToNextPage": "Idź do kolejnej strony", + "PagerGoToPage": "Idź do strony {page} z {totalPages}", + "PagerGoToPreviousPage": "Idź do poprzedniej strony", + "Parse": "Parsuj", + "ParseModalErrorParsing": "Błąd analizy, spróbuj ponownie.", + "ParseModalHelpText": "Wpisz tytuł wydania w polu powyżej", + "ParseModalHelpTextDetails": "{appName} spróbuje przeanalizować tytuł i pokaże szczegóły", + "ParseModalUnableToParse": "Nie można przeanalizować podanego tytułu, spróbuj ponownie.", + "PartialSeason": "Częściowy sezon", + "Password": "Hasło", + "PasswordConfirmation": "Potwierdzenie hasła", + "Path": "Ścieżka", + "Paused": "Wstrzymane", + "Peers": "Peerzy", + "Pending": "Oczekujące", + "PendingChangesDiscardChanges": "Odrzuć zmiany i wyjdź", + "PendingChangesMessage": "Masz niezapisane zmiany, czy na pewno chcesz opuścić tę stronę?", + "PendingChangesStayReview": "Zostań i przejrzyj zmiany", + "PendingDownloadClientUnavailable": "Oczekujące - klient pobierania jest niedostępny", + "Period": "Okres", + "Permissions": "Uprawnienia", + "Port": "Port", + "PortNumber": "Numer portu", + "PostImportCategory": "Kategoria po imporcie", + "PosterOptions": "Opcje plakatu", + "PosterSize": "Rozmiar plakatu", + "Posters": "Plakaty", + "PreferAndUpgrade": "Preferuj i aktualizuj", + "PreferProtocol": "Preferuj {preferredProtocol}", + "PreferTorrent": "Preferuj torrenty", + "PreferUsenet": "Preferuj Usenet", + "Preferred": "Preferowany", + "PreferredProtocol": "Preferowany protokół", + "PreferredSize": "Preferowany rozmiar", + "PrefixedRange": "Zakres z prefiksem", + "Premiere": "Premiera", + "Presets": "Presety", + "PreviewRename": "Podgląd zmiany nazwy", + "PreviewRenameSeason": "Podgląd zmiany nazwy dla tego sezonu", + "PreviousAiring": "Poprzednia emisja", + "PreviousAiringDate": "Poprzednia emisja: {date}", + "PreviouslyInstalled": "Wcześniej zainstalowane", + "Priority": "Priorytet", + "PrioritySettings": "Priorytet: {priority}", + "ProcessingFolders": "Przetwarzanie folderów", + "Profiles": "Profile", + "ProfilesSettingsSummary": "Profile jakości, opóźnienia języka i wydań", + "Progress": "Postęp", + "ProgressBarProgress": "Pasek postępu: {progress}%", + "Proper": "Właściwy", + "Protocol": "Protokół", + "ProtocolHelpText": "Wybierz protokół(y) do użycia oraz ten preferowany przy wyborze między wydaniami równorzędnymi", + "Proxy": "Proxy", + "ProxyBadRequestHealthCheckMessage": "Nie udało się przetestować proxy. Kod statusu: {statusCode}", + "ProxyBypassFilterHelpText": "Użyj ',' jako separatora i '*.' jako wildcard dla subdomen", + "ProxyFailedToTestHealthCheckMessage": "Nie udało się przetestować proxy: {url}", + "ProxyPasswordHelpText": "Nazwę użytkownika i hasło wpisz tylko wtedy, gdy są wymagane. W przeciwnym razie pozostaw puste.", + "ProxyResolveIpHealthCheckMessage": "Nie udało się rozwiązać adresu IP dla skonfigurowanego hosta proxy {proxyHostName}", + "ProxyType": "Typ proxy", + "ProxyUsernameHelpText": "Nazwę użytkownika i hasło wpisz tylko wtedy, gdy są wymagane. W przeciwnym razie pozostaw puste.", + "PublishedDate": "Data publikacji", + "Qualities": "Jakości", + "QualitiesHelpText": "Jakości wyżej na liście są bardziej preferowane (nawet jeśli nie są zaznaczone). Jakości w tej samej grupie są równorzędne. Pożądane są tylko zaznaczone jakości", + "QualitiesLoadError": "Nie można wczytać jakości", + "Quality": "Jakość", + "QualityCutoffNotMet": "Próg jakości nie został osiągnięty", + "QualityDefinitions": "Definicje jakości", + "QualityDefinitionsLoadError": "Nie można wczytać definicji jakości", + "QualityDefinitionsSizeNotice": "Ograniczenia rozmiaru zostały przeniesione do profili jakości", + "QualityProfile": "Profil jakości", + "QualityProfileInUseSeriesListCollection": "Nie można usunąć profilu jakości przypisanego do serialu, listy lub kolekcji", + "QualityProfiles": "Profile jakości", + "QualityProfilesLoadError": "Nie można wczytać profili jakości", + "QualitySettings": "Ustawienia jakości", + "QualitySettingsSummary": "Rozmiary jakości i nazewnictwo", + "Queue": "Kolejka", + "QueueFilterHasNoItems": "Wybrany filtr kolejki nie zawiera elementów", + "QueueIsEmpty": "Kolejka jest pusta", + "QueueItem": "1 element kolejki", + "QueueItems": "{count} elementów kolejki", + "QueueLoadError": "Nie udało się wczytać kolejki", + "Queued": "W kolejce", + "QuickSearch": "Szybkie wyszukiwanie", + "Range": "Zakres", + "Rating": "Ocena", + "RatingVotes": "Liczba głosów", + "ReadTheWikiForMoreInformation": "Przeczytaj Wiki, aby uzyskać więcej informacji", + "Real": "Real", + "Reason": "Powód", + "RecentChanges": "Ostatnie zmiany", + "RecentFolders": "Ostatnie foldery", + "RecycleBinUnableToWriteHealthCheckMessage": "Nie można zapisywać do skonfigurowanego folderu kosza: {path}. Upewnij się, że ścieżka istnieje i użytkownik uruchamiający {appName} ma do niej uprawnienia zapisu", + "RecyclingBin": "Kosz", + "RecyclingBinCleanup": "Czyszczenie kosza", + "RecyclingBinCleanupHelpText": "Ustaw 0, aby wyłączyć automatyczne czyszczenie", + "RecyclingBinCleanupHelpTextWarning": "Pliki w koszu starsze niż wybrana liczba dni będą automatycznie usuwane", + "RecyclingBinHelpText": "Pliki będą trafiać tutaj po usunięciu zamiast być usuwane trwale", + "Refresh": "Odśwież", + "RefreshAndScan": "Odśwież i skanuj", + "RefreshAndScanTooltip": "Odśwież informacje i przeskanuj dysk", + "RefreshSeries": "Odśwież serial", + "RegularExpression": "Wyrażenie regularne", + "RegularExpressionsCanBeTested": "Wyrażenia regularne można testować [tutaj]({url}).", + "RegularExpressionsTutorialLink": "Więcej informacji o wyrażeniach regularnych znajdziesz [tutaj]({url}).", + "RejectionCount": "Liczba odrzuceń", + "Rejections": "Odrzucenia", + "RelativePath": "Ścieżka względna", + "Release": "Wydanie", + "ReleaseGroup": "Grupa wydania", + "ReleaseGroupFootNote": "Opcjonalnie kontroluj obcinanie do maksymalnej liczby bajtów, łącznie z wielokropkiem (`...`). Obsługiwane jest obcinanie od końca (np. `{Release Group:30}`) lub od początku (np. `{Release Group:-30}`).", + "ReleaseGroups": "Grupy wydań", + "ReleaseHash": "Hash wydania", + "ReleaseProfile": "Profil wydań", + "ReleaseProfileExcludedTagSeriesHelpText": "Profile wydań nie będą stosowane do seriali z co najmniej jednym pasującym tagiem.", + "ReleaseProfileIndexerHelpText": "Określ, do którego indeksera ma zastosowanie profil", + "ReleaseProfileIndexerHelpTextWarning": "Ustawienie konkretnego indeksera w profilu wydań spowoduje, że profil będzie stosowany tylko do wydań z tego indeksera.", + "ReleaseProfileTagSeriesHelpText": "Profile wydań będą stosowane do seriali z co najmniej jednym pasującym tagiem. Pozostaw puste, aby stosować do wszystkich seriali", + "ReleaseProfiles": "Profile wydań", + "ReleaseProfilesLoadError": "Nie można wczytać profili wydań", + "ReleasePush": "Push wydania", + "ReleaseRejected": "Wydanie odrzucone", + "ReleaseSceneIndicatorAssumingScene": "Założono numerację Scene.", + "ReleaseSceneIndicatorAssumingTvdb": "Założono numerację TVDB.", + "ReleaseSceneIndicatorMappedNotRequested": "Mapowany odcinek nie był żądany w tym wyszukiwaniu.", + "ReleaseSceneIndicatorSourceMessage": "{message} wydania mają niejednoznaczną numerację, nie można wiarygodnie zidentyfikować odcinka.", + "ReleaseSceneIndicatorUnknownMessage": "Numeracja różni się dla tego odcinka, a wydanie nie pasuje do żadnych znanych mapowań.", + "ReleaseSceneIndicatorUnknownSeries": "Nieznany odcinek lub serial.", + "ReleaseSource": "Źródło wydania", + "ReleaseTitle": "Tytuł wydania", + "ReleaseType": "Typ wydania", + "Reload": "Przeładuj", + "RemotePath": "Ścieżka zdalna", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Używasz Dockera; klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź mapowania ścieżek zdalnych i ustawienia klienta pobierania.", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Używasz Dockera; klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale ten katalog nie wydaje się istnieć wewnątrz kontenera. Sprawdź mapowania ścieżek zdalnych i ustawienia wolumenów kontenera.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} widzi pobrany odcinek {path}, ale nie ma do niego dostępu. Prawdopodobny błąd uprawnień.", + "RemotePathMappingFileRemovedHealthCheckMessage": "Plik {path} został usunięty w trakcie przetwarzania.", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Używasz Dockera; klient pobierania {downloadClientName} zgłosił pliki w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź mapowania ścieżek zdalnych i ustawienia klienta pobierania.", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "Klient pobierania {downloadClientName} zgłosił pliki w {path}, ale {appName} nie widzi tego katalogu. Może być konieczna korekta uprawnień folderu.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Lokalny klient pobierania {downloadClientName} zgłosił pliki w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź ustawienia klienta pobierania.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Zdalny klient pobierania {downloadClientName} zgłosił pliki w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź mapowania ścieżek zdalnych i ustawienia klienta pobierania.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} widzi katalog pobierania {downloadPath}, ale nie ma do niego dostępu. Prawdopodobny błąd uprawnień.", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "Klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale {appName} nie widzi tego katalogu. Może być konieczna korekta uprawnień folderu.", + "RemotePathMappingHostHelpText": "Ten sam host, który został podany dla zdalnego klienta pobierania", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} nie zdołał zaimportować odcinka/odcinków. Sprawdź logi, aby poznać szczegóły.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Zdalny klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale ten katalog nie wydaje się istnieć. Prawdopodobnie brakuje mapowania ścieżki zdalnej lub jest ono błędne.", + "RemotePathMappingLocalPathHelpText": "Ścieżka, której {appName} powinien używać do lokalnego dostępu do ścieżki zdalnej", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Lokalny klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź ustawienia klienta pobierania.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Zdalny klient pobierania {downloadClientName} zgłosił pliki w {path}, ale ten katalog nie wydaje się istnieć. Prawdopodobnie brakuje mapowania ścieżki zdalnej.", + "RemotePathMappingRemotePathHelpText": "Ścieżka główna katalogu, do którego dostęp ma klient pobierania", + "RemotePathMappingWrongOSPathHealthCheckMessage": "Zdalny klient pobierania {downloadClientName} zapisuje pobrania w {path}, ale to nie jest prawidłowa ścieżka {osName}. Sprawdź mapowania ścieżek zdalnych i ustawienia klienta pobierania.", + "RemotePathMappings": "Mapowania ścieżek zdalnych", + "RemotePathMappingsInfo": "Mapowania ścieżek zdalnych są bardzo rzadko wymagane. Jeśli {appName} i klient pobierania działają na tym samym systemie, lepiej dopasować ścieżki. Więcej informacji w [wiki]({wikiLink})", + "RemotePathMappingsLoadError": "Nie można wczytać mapowań ścieżek zdalnych", + "Remove": "Usuń", + "RemoveCompleted": "Usuń ukończone", + "RemoveCompletedDownloads": "Usuń ukończone pobrania", + "RemoveCompletedDownloadsHelpText": "Usuń zaimportowane pobrania z historii klienta pobierania", + "RemoveFailed": "Usuń nieudane", + "RemoveFailedDownloads": "Usuń nieudane pobrania", + "RemoveFailedDownloadsHelpText": "Usuń nieudane pobrania z historii klienta pobierania", + "RemoveFilter": "Usuń filtr", + "RemoveFromBlocklist": "Usuń z czarnej listy", + "RemoveFromDownloadClient": "Usuń z klienta pobierania", + "RemoveFromDownloadClientHint": "Usuwa pobranie i plik(i) z klienta pobierania", + "RemoveFromQueue": "Usuń z kolejki", + "RemoveMultipleFromDownloadClientHint": "Usuwa pobrania i pliki z klienta pobierania", + "RemoveQueueItem": "Usuń - {sourceTitle}", + "RemoveQueueItemConfirmation": "Czy na pewno chcesz usunąć '{sourceTitle}' z kolejki?", + "RemoveQueueItemRemovalMethod": "Metoda usuwania", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Usuń z klienta pobierania' usunie pobranie i plik(i) z klienta pobierania.", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Usuń z klienta pobierania' usunie pobrania i pliki z klienta pobierania.", + "RemoveRootFolder": "Usuń folder główny", + "RemoveRootFolderWithSeriesMessageText": "Czy na pewno chcesz usunąć folder główny '{path}'? Pliki i foldery nie zostaną usunięte z dysku, a seriale w tym folderze głównym nie zostaną usunięte z {appName}.", + "RemoveSelected": "Usuń zaznaczone", + "RemoveSelectedBlocklistMessageText": "Czy na pewno chcesz usunąć zaznaczone elementy z czarnej listy?", + "RemoveSelectedItem": "Usuń zaznaczony element", + "RemoveSelectedItemQueueMessageText": "Czy na pewno chcesz usunąć 1 element z kolejki?", + "RemoveSelectedItems": "Usuń zaznaczone elementy", + "RemoveSelectedItemsQueueMessageText": "Czy na pewno chcesz usunąć {selectedCount} elementów z kolejki?", + "RemoveTagsAutomatically": "Usuwaj tagi automatycznie", + "RemoveTagsAutomaticallyHelpText": "Usuwaj tagi automatycznie, jeśli warunki nie są spełnione", + "RemovedFromTaskQueue": "Usunięto z kolejki zadań", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Seriale {series} zostały usunięte z TheTVDB", + "RemovedSeriesSingleRemovedHealthCheckMessage": "Serial {series} został usunięty z TheTVDB", + "RemovingTag": "Usuwanie tagu", + "RenameEpisodes": "Zmień nazwy odcinków", + "RenameEpisodesHelpText": "{appName} użyje istniejącej nazwy pliku, jeśli zmiana nazw jest wyłączona", + "RenameFiles": "Zmień nazwy plików", + "Renamed": "Zmieniono nazwę", + "Reorder": "Zmień kolejność", + "Repack": "Przepakowany", + "Repeat": "Powtórz", + "Replace": "Zastąp", + "ReplaceIllegalCharacters": "Zastąp niedozwolone znaki", + "ReplaceIllegalCharactersHelpText": "Zastępuj niedozwolone znaki. Jeśli odznaczone, {appName} będzie je usuwać", + "ReplaceWithDash": "Zastąp myślnikiem", + "ReplaceWithSpaceDash": "Zastąp spacją i myślnikiem", + "ReplaceWithSpaceDashSpace": "Zastąp spacją-myślnikiem-spacją", + "Required": "Wymagane", + "RequiredHelpText": "Ten warunek {implementationName} musi pasować, aby format niestandardowy został zastosowany. W przeciwnym razie wystarczy pojedyncze dopasowanie {implementationName}.", + "RescanAfterRefreshHelpTextWarning": "{appName} nie będzie automatycznie wykrywać zmian w plikach, jeśli nie ustawiono 'Always'", + "RescanAfterRefreshSeriesHelpText": "Przeskanuj folder serialu po odświeżeniu serialu", + "RescanSeriesFolderAfterRefresh": "Przeskanuj folder serialu po odświeżeniu", + "Reset": "Resetuj", + "ResetAPIKey": "Resetuj klucz API", + "ResetAPIKeyMessageText": "Czy na pewno chcesz zresetować klucz API?", + "ResetDefinitionTitlesHelpText": "Zresetuj tytuły definicji oraz wartości", + "ResetDefinitions": "Resetuj definicje", + "ResetQualityDefinitions": "Resetuj definicje jakości", + "ResetQualityDefinitionsMessageText": "Czy na pewno chcesz zresetować definicje jakości?", + "Restart": "Uruchom ponownie", + "RestartLater": "Uruchomię ponownie później", + "RestartNow": "Uruchom teraz", + "RestartReloadNote": "Uwaga: podczas przywracania {appName} automatycznie uruchomi się ponownie i przeładuje interfejs.", + "RestartRequiredHelpTextWarning": "Wymaga ponownego uruchomienia, aby zastosować", + "RestartRequiredToApplyChanges": "{appName} wymaga ponownego uruchomienia, aby zastosować zmiany. Uruchomić teraz?", + "RestartRequiredWindowsService": "W zależności od użytkownika uruchamiającego usługę {appName}, może być konieczne jednokrotne uruchomienie {appName} jako administrator, zanim usługa zacznie uruchamiać się automatycznie.", + "RestartSonarr": "Uruchom ponownie {appName}", + "Restore": "Przywróć", + "RestoreBackup": "Przywróć kopię zapasową", + "RestrictionsLoadError": "Nie można wczytać ograniczeń", + "Result": "Wynik", + "Retention": "Retencja", + "RetentionHelpText": "Tylko Usenet: ustaw na 0, aby retencja była nieograniczona", + "RetryingDownloadOn": "Ponowna próba pobrania: {date} o {time}", + "RootFolder": "Folder główny", + "RootFolderEmptyHealthCheckMessage": "Pusty folder główny: {rootFolderPath}", + "RootFolderMissingHealthCheckMessage": "Brak folderu głównego: {rootFolderPath}", + "RootFolderMultipleEmptyHealthCheckMessage": "Wiele folderów głównych jest pustych: {rootFolderPaths}", + "RootFolderMultipleMissingHealthCheckMessage": "Brakuje wielu folderów głównych: {rootFolderPaths}", + "RootFolderPath": "Ścieżka folderu głównego", + "RootFolderSelectFreeSpace": "Wolne: {freeSpace}", + "RootFolders": "Foldery główne", + "RootFoldersLoadError": "Nie można wczytać folderów głównych", + "Rss": "RSS", + "RssIsNotSupportedWithThisIndexer": "RSS nie jest obsługiwane przez ten indekser", + "RssSync": "Synchronizacja RSS", + "RssSyncInterval": "Interwał synchronizacji RSS", + "RssSyncIntervalHelpText": "Interwał w minutach. Ustaw 0, aby wyłączyć (zatrzyma to wszystkie automatyczne pobierania wydań)", + "RssSyncIntervalHelpTextWarning": "Dotyczy wszystkich indekserów, stosuj się do ich zasad", + "Run": "Uruchom", + "Runtime": "Czas trwania", + "Saturday": "Sobota", + "Save": "Zapisz", + "SaveChanges": "Zapisz zmiany", + "SaveSettings": "Zapisz ustawienia", + "Scene": "Sceny", + "SceneInfo": "Informacje o scenie", + "SceneInformation": "Informacje o scenie", + "SceneNumberNotVerified": "Numeracja sceny nie została jeszcze zweryfikowana", + "SceneNumbering": "Numeracja sceny", + "Scheduled": "Zaplanowane", + "Score": "Wynik", + "Script": "Skrypt", + "ScriptPath": "Ścieżka skryptu", + "Search": "Szukaj", + "SearchAll": "Szukaj wszystkiego", + "SearchByTvdbId": "Możesz też szukać po ID TVDB serialu, np. tvdb:71663", + "SearchFailedError": "Wyszukiwanie nie powiodło się, spróbuj ponownie później.", + "SearchForAllMissingEpisodes": "Szukaj wszystkich brakujących odcinków", + "SearchForAllMissingEpisodesConfirmationCount": "Czy na pewno chcesz wyszukać wszystkie {totalRecords} brakujące odcinki?", + "SearchForCutoffUnmetEpisodes": "Szukaj wszystkich odcinków niespełniających progu", + "SearchForCutoffUnmetEpisodesConfirmationCount": "Czy na pewno chcesz wyszukać wszystkie {totalRecords} odcinki niespełniające progu?", + "SearchForMissing": "Szukaj brakujących", + "SearchForMonitoredEpisodes": "Szukaj monitorowanych odcinków", + "SearchForMonitoredEpisodesSeason": "Szukaj monitorowanych odcinków w tym sezonie", + "SearchForQuery": "Szukaj: {query}", + "SearchIsNotSupportedWithThisIndexer": "Wyszukiwanie nie jest obsługiwane przez ten indekser", + "SearchMonitored": "Szukaj monitorowanych", + "SearchSelected": "Szukaj zaznaczonych", + "Season": "Sezon", + "SeasonCount": "Liczba sezonów", + "SeasonDetails": "Szczegóły sezonu", + "SeasonFinale": "Finał sezonu", + "SeasonFolder": "Folder sezonu", + "SeasonFolderFormat": "Format folderu sezonu", + "SeasonInformation": "Informacje o sezonie", + "SeasonNumber": "Numer sezonu", + "SeasonNumberToken": "Sezon {seasonNumber}", + "SeasonPack": "Paczka sezonu", + "SeasonPackUpgradeAllowAnyWarning": "Zezwalaj na paczkę sezonu, jeśli ulepsza dowolny odcinek. Dotyczy to wszystkich źródeł automatycznych pobrań.", + "SeasonPackUpgradeAllowHelpText": "Wymagaj, aby paczka sezonu była ulepszeniem jakości lub formatu niestandardowego dla wszystkich odcinków", + "SeasonPackUpgradeAllowLabel": "Zezwalaj na ulepszenia paczek sezonu", + "SeasonPackUpgradeThresholdHelpText": "Wymagaj, aby paczka sezonu była ulepszeniem co najmniej dla X procent odcinków.", + "SeasonPackUpgradeThresholdHelpTextExample": "{numberEpisodes} z {totalEpisodes} odcinków: {count}%", + "SeasonPackUpgradeThresholdLabel": "Próg ulepszenia paczki sezonu", + "SeasonPassEpisodesDownloaded": "Pobrane odcinki: {episodeFileCount}/{totalEpisodeCount}", + "SeasonPassTruncated": "Pokazano tylko 25 najnowszych sezonów, przejdź do szczegółów, aby zobaczyć wszystkie", + "SeasonPremiere": "Premiera sezonu", + "SeasonPremieresOnly": "Tylko premiery sezonów", + "Seasons": "Sezony", + "SeasonsMonitoredAll": "Wszystkie", + "SeasonsMonitoredNone": "Żadne", + "SeasonsMonitoredStatus": "Monitorowanie sezonów", + "SecretToken": "Sekretny token", + "Security": "Bezpieczeństwo", + "Seeders": "Seederzy", + "SelectAll": "Zaznacz wszystko", + "SelectDownloadClientModalTitle": "{modalTitle} - wybierz klienta pobierania", + "SelectDropdown": "Wybierz...", + "SelectEpisodes": "Wybierz odcinki", + "SelectEpisodesModalTitle": "{modalTitle} - wybierz odcinki", + "SelectFolder": "Wybierz folder", + "SelectFolderModalTitle": "{modalTitle} - wybierz folder", + "SelectIndexerFlags": "Wybierz flagi indekswera", + "SelectLanguage": "Wybierz język", + "SelectLanguageModalTitle": "{modalTitle} - wybierz język", + "SelectLanguages": "Wybierz języki", + "SelectQuality": "Wybierz jakość", + "SelectReleaseGroup": "Wybierz grupę wydania", + "SelectReleaseType": "Wybierz typ wydania", + "SelectSeason": "Wybierz sezon", + "SelectSeasonModalTitle": "{modalTitle} - wybierz sezon", + "SelectSeries": "Wybierz serial", + "SendAnonymousUsageData": "Wysyłaj anonimowe dane użycia", + "Series": "Seriale", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Informacje o serialach i odcinkach dostarcza TheTVDB.com. [Rozważ ich wsparcie]({url}).", + "SeriesCannotBeFound": "Niestety nie można znaleźć tego serialu.", + "SeriesDetailsCountEpisodeFiles": "Pliki odcinków: {episodeFileCount}", + "SeriesDetailsGoTo": "Przejdź do {title}", + "SeriesDetailsNoEpisodeFiles": "Brak plików odcinków", + "SeriesDetailsOneEpisodeFile": "1 plik odcinka", + "SeriesDetailsRuntime": "{runtime} minut", + "SeriesEditRootFolderHelpText": "Przeniesienie serialu do tego samego folderu głównego może służyć do zmiany nazw folderów seriali zgodnie z nowym tytułem lub formatem nazewnictwa", + "SeriesEditor": "Edytor seriali", + "SeriesFinale": "Finał serialu", + "SeriesFolderFormat": "Format folderu serialu", + "SeriesFolderFormatHelpText": "Używane przy dodawaniu nowego serialu lub przenoszeniu seriali przez edytor seriali", + "SeriesFolderImportedTooltip": "Odcinek zaimportowany z folderu serialu", + "SeriesFootNote": "Opcjonalnie kontroluj obcinanie do maksymalnej liczby bajtów, łącznie z wielokropkiem (`...`). Obsługiwane jest obcinanie od końca (np. `{Series Title:30}`) lub od początku (np. `{Series Title:-30}`).", + "SeriesID": "ID serialu", + "SeriesInImportListExclusions": "Serial znajduje się na liście wykluczeń importu", + "SeriesIndexFooterContinuing": "Kontynuowany (wszystkie odcinki pobrane)", + "SeriesIndexFooterDownloading": "Pobieranie (co najmniej jeden odcinek)", + "SeriesIndexFooterEnded": "Zakończony (wszystkie odcinki pobrane)", + "SeriesIndexFooterMissingMonitored": "Brakujące odcinki (serial monitorowany)", + "SeriesIndexFooterMissingUnmonitored": "Brakujące odcinki (serial niemonitorowany)", + "SeriesIsMonitored": "Serial jest monitorowany", + "SeriesIsUnmonitored": "Serial nie jest monitorowany", + "SeriesLoadError": "Nie można wczytać seriali", + "SeriesMatchType": "Typ dopasowania serialu", + "SeriesMonitoring": "Monitorowanie serialu", + "SeriesPremiere": "Premiera serialu", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Łącznie: {totalEpisodeCount}, Pobieranie: {downloadingCount})", + "SeriesTitle": "Tytuł serialu", + "SeriesTitleToExcludeHelpText": "Nazwa serialu do wykluczenia", + "SeriesType": "Typ serialu", + "SeriesTypes": "Typy seriali", + "SeriesTypesHelpText": "Typ serialu jest używany do zmiany nazwy, analizy i wyszukiwania", + "SetIndexerFlags": "Ustaw flagi indekswera", + "SetIndexerFlagsModalTitle": "{modalTitle} - ustaw flagi indekswera", + "SetPermissions": "Ustaw uprawnienia", + "SetPermissionsLinuxHelpText": "Czy uruchamiać chmod podczas importu/zmiany nazw plików?", + "SetPermissionsLinuxHelpTextWarning": "Jeśli nie wiesz, do czego służą te ustawienia, nie zmieniaj ich.", + "SetReleaseGroup": "Ustaw grupę wydania", + "SetReleaseGroupModalTitle": "{modalTitle} - ustaw grupę wydania", + "SetTags": "Ustaw tagi", + "Settings": "Ustawienia", + "ShortDateFormat": "Krótki format daty", + "ShowAdvanced": "Pokaż zaawansowane", + "ShowBanners": "Pokaż bannery", + "ShowBannersHelpText": "Pokaż bannery zamiast tytułów", + "ShowDateAdded": "Pokaż datę dodania", + "ShowEpisodeInformation": "Pokaż informacje o odcinku", + "ShowEpisodeInformationHelpText": "Pokaż tytuł i numer odcinka", + "ShowEpisodes": "Pokaż odcinki", + "ShowMonitored": "Pokaż monitorowane", + "ShowMonitoredHelpText": "Pokaż status monitorowania pod plakatem", + "ShowNetwork": "Pokaż stację", + "ShowPath": "Pokaż ścieżkę", + "ShowPreviousAiring": "Pokaż poprzednią emisję", + "ShowQualityProfile": "Pokaż profil jakości", + "ShowQualityProfileHelpText": "Pokaż profil jakości pod plakatem", + "ShowRelativeDates": "Pokaż daty względne", + "ShowRelativeDatesHelpText": "Pokaż daty względne (Dzisiaj/Wczoraj/itp.) albo bezwzględne", + "ShowSearch": "Pokaż wyszukiwanie", + "ShowSearchHelpText": "Pokaż przycisk wyszukiwania po najechaniu", + "ShowSeasonCount": "Pokaż liczbę sezonów", + "ShowSeriesTitleHelpText": "Pokaż tytuł serialu pod plakatem", + "ShowSizeOnDisk": "Pokaż rozmiar na dysku", + "ShowTags": "Pokaż tagi", + "ShowTagsHelpText": "Pokaż tagi pod plakatem", + "ShowTitle": "Pokaż tytuł", + "ShownClickToHide": "Widoczne, kliknij aby ukryć", + "Shutdown": "Wyłącz", + "SingleEpisode": "Pojedynczy odcinek", + "SingleEpisodeInvalidFormat": "Pojedynczy odcinek: nieprawidłowy format", + "Size": "Rozmiar", + "SizeLimit": "Limit rozmiaru", + "SizeOnDisk": "Rozmiar na dysku", + "SkipFreeSpaceCheck": "Pomiń sprawdzanie wolnego miejsca", + "SkipFreeSpaceCheckHelpText": "Użyj, gdy {appName} nie może wykryć wolnego miejsca w folderze głównym", + "SkipRedownload": "Pomiń ponowne pobieranie", + "SkipRedownloadHelpText": "Zapobiega próbom pobrania alternatywnego wydania dla tego elementu przez {appName}", + "Small": "Mały", + "SmartReplace": "Inteligentna zamiana", + "SmartReplaceHint": "Myślnik lub spacja-myślnik zależnie od nazwy", + "Socks4": "Socks4", + "Socks5": "Socks5 (obsługa TOR)", + "SomeResultsAreHiddenByTheAppliedFilter": "Część wyników jest ukryta przez zastosowany filtr", + "SonarrTags": "Tagi {appName}", + "Sort": "Sortuj", + "Source": "Źródło", + "SourcePath": "Ścieżka źródłowa", + "SourceRelativePath": "Względna ścieżka źródłowa", + "SourceTitle": "Tytuł źródła", + "Space": "Spacja", + "Special": "Specjalny", + "SpecialEpisode": "Odcinek specjalny", + "Specials": "Odcinki specjalne", + "SpecialsFolderFormat": "Format folderu odcinków specjalnych", + "SslCertPassword": "Hasło certyfikatu SSL", + "SslCertPasswordHelpText": "Hasło do pliku pfx", + "SslCertPath": "Ścieżka certyfikatu SSL", + "SslCertPathHelpText": "Ścieżka do pliku pfx lub pem", + "SslKeyPath": "Ścieżka klucza SSL", + "SslKeyPathHelpText": "Ścieżka do pliku klucza używanego z plikiem pem", + "SslPort": "Port SSL", + "Standard": "Standard", + "StandardEpisodeFormat": "Standardowy format odcinka", + "StandardEpisodeTypeDescription": "Odcinki wydane we wzorcu SxxEyy", + "StandardEpisodeTypeFormat": "Numery sezonu i odcinka ({format})", + "StartImport": "Rozpocznij import", + "StartProcessing": "Rozpocznij przetwarzanie", + "Started": "Rozpoczęto", + "StartupDirectory": "Katalog startowy", + "Status": "Status", + "StopSelecting": "Zakończ zaznaczanie", + "Style": "Styl", + "SubtitleLanguages": "Języki napisów", + "Sunday": "Niedziela", + "SupportedAutoTaggingProperties": "{appName} obsługuje następujące właściwości dla reguł automatycznego tagowania", + "SupportedCustomConditions": "{appName} obsługuje niestandardowe warunki dla poniższych właściwości wydań.", + "SupportedDownloadClients": "{appName} obsługuje wielu popularnych klientów pobierania torrentów i Usenetu.", + "SupportedDownloadClientsMoreInfo": "Aby uzyskać więcej informacji o poszczególnych klientach pobierania, kliknij przyciski więcej informacji.", + "SupportedImportListsMoreInfo": "Aby uzyskać więcej informacji o poszczególnych listach importu, kliknij przyciski więcej informacji.", + "SupportedIndexers": "{appName} obsługuje każdy indekser korzystający ze standardu Newznab oraz inne indeksery wymienione poniżej.", + "SupportedIndexersMoreInfo": "Aby uzyskać więcej informacji o poszczególnych indekserach, kliknij przyciski więcej informacji.", + "SupportedListsMoreInfo": "Aby uzyskać więcej informacji o poszczególnych listach, kliknij przyciski więcej informacji.", + "SupportedListsSeries": "{appName} obsługuje wiele list do importowania seriali do bazy danych.", + "System": "System", + "SystemDefault": "Domyślne systemowe", + "SystemTimeHealthCheckMessage": "Czas systemowy różni się o więcej niż 1 dzień. Zaplanowane zadania mogą nie działać poprawnie, dopóki czas nie zostanie skorygowany", + "Table": "Tabela", + "TableColumns": "Kolumny", + "TableColumnsHelpText": "Wybierz, które kolumny są widoczne i w jakiej kolejności", + "TableOptions": "Opcje tabeli", + "TableOptionsButton": "Przycisk opcji tabeli", + "TablePageSize": "Rozmiar strony", + "TablePageSizeHelpText": "Liczba elementów wyświetlanych na stronie", + "TablePageSizeMaximum": "Rozmiar strony nie może przekraczać {maximumValue}", + "TablePageSizeMinimum": "Rozmiar strony musi wynosić co najmniej {minimumValue}", + "TagCannotBeDeletedWhileInUse": "Tag nie może zostać usunięty podczas użycia", + "TagDetails": "Szczegóły tagu - {label}", + "TagIsNotUsedAndCanBeDeleted": "Tag nie jest używany i można go usunąć", + "Tags": "Tagi", + "TagsLoadError": "Nie można wczytać tagów", + "TagsSettingsSummary": "Zobacz wszystkie tagi i sposób ich użycia. Nieużywane tagi można usunąć", + "TaskUserAgentTooltip": "User-Agent przekazany przez aplikację, która wywołała API", + "Tasks": "Zadania", + "Tba": "Będzie ogłoszone", + "Test": "Test", + "TestAll": "Testuj wszystko", + "TestAllClients": "Testuj wszystkich klientów", + "TestAllIndexers": "Testuj wszystkie indeksery", + "TestAllLists": "Testuj wszystkie listy", + "TestParsing": "Testuj analizę", + "TheLogLevelDefault": "Domyślny poziom logów to 'Debug' i można go zmienić w [Ustawieniach ogólnych](/settings/general)", + "TheTvdb": "TheTVDB", + "Theme": "Motyw", + "ThemeHelpText": "Zmień motyw interfejsu aplikacji. Motyw 'Auto' użyje motywu systemu operacyjnego, aby ustawić tryb jasny lub ciemny. Inspirowane Theme.Park", + "Threshold": "Próg", + "Thursday": "Czwartek", + "Time": "Czas", + "TimeFormat": "Format czasu", + "TimeLeft": "Pozostały czas", + "TimeZone": "Strefa czasowa", + "Title": "Tytuł", + "Titles": "Tytuły", + "Today": "Dzisiaj", + "TodayAt": "Dzisiaj o {time}", + "ToggleMonitoredSeriesUnmonitored": "Nie można przełączyć stanu monitorowania, gdy serial jest niemonitorowany", + "ToggleMonitoredToUnmonitored": "Monitorowany, kliknij aby przestać monitorować", + "ToggleUnmonitoredToMonitored": "Niemonitorowany, kliknij aby monitorować", + "Tomorrow": "Jutro", + "TomorrowAt": "Jutro o {time}", + "TorrentBlackhole": "Czarna dziura torrentów", + "TorrentBlackholeSaveMagnetFiles": "Zapisuj pliki magnet", + "TorrentBlackholeSaveMagnetFilesExtension": "Rozszerzenie plików magnet", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Rozszerzenie używane dla linków magnet, domyślnie '.magnet'", + "TorrentBlackholeSaveMagnetFilesHelpText": "Zapisz link magnet, jeśli plik .torrent nie jest dostępny (przydatne tylko, jeśli klient pobierania obsługuje magnety zapisane do pliku)", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Tylko do odczytu", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Zamiast przenoszenia plików, {appName} wykona kopiowanie lub hardlink (zależnie od ustawień/konfiguracji systemu)", + "TorrentBlackholeTorrentFolder": "Folder torrentów", + "TorrentDelay": "Opóźnienie torrentów", + "TorrentDelayHelpText": "Opóźnienie w minutach przed pobraniem torrenta", + "TorrentDelayTime": "Opóźnienie torrentów: {torrentDelay}", + "Torrents": "Torrenty", + "TorrentsDisabled": "Torrenty wyłączone", + "Total": "Łącznie", + "TotalFileSize": "Łączny rozmiar plików", + "TotalRecords": "Łączna liczba rekordów: {totalRecords}", + "TotalSpace": "Łączna przestrzeń", + "Trace": "Ślad", + "True": "Prawda", + "Tuesday": "Wtorek", + "TvdbId": "ID TVDB", + "TvdbIdExcludeHelpText": "ID TVDB serialu do wykluczenia", + "Twitter": "Twitter", + "Type": "Typ", + "TypeOfList": "Lista {typeOfList}", + "Ui": "Interfejs", + "UiLanguage": "Język interfejsu", + "UiLanguageHelpText": "Język, którego {appName} będzie używać w interfejsie", + "UiSettings": "Ustawienia interfejsu", + "UiSettingsLoadError": "Nie można wczytać ustawień interfejsu", + "UiSettingsSummary": "Kalendarz, data i opcje dla osób z zaburzeniami rozpoznawania kolorów", + "Umask": "Umask", + "Umask750Description": "{octal} - właściciel: zapis, grupa: odczyt", + "Umask755Description": "{octal} - właściciel: zapis, pozostali: odczyt", + "Umask770Description": "{octal} - właściciel i grupa: zapis", + "Umask775Description": "{octal} - właściciel i grupa: zapis, pozostali: odczyt", + "Umask777Description": "{octal} - wszyscy: zapis", + "UnableToImportAutomatically": "Nie można zaimportować automatycznie", + "UnableToLoadAutoTagging": "Nie można wczytać automatycznego tagowania", + "UnableToLoadBackups": "Nie można wczytać kopii zapasowych", + "UnableToUpdateSonarrDirectly": "Nie można zaktualizować {appName} bezpośrednio,", + "Unavailable": "Niedostępne", + "Underscore": "Podkreślenie", + "Ungroup": "Rozgrupuj", + "Unknown": "Nieznane", + "UnknownDownloadState": "Nieznany stan pobierania: {state}", + "UnknownEventTooltip": "Nieznane zdarzenie", + "UnknownSeriesItems": "Nieznane elementy seriali", + "Unlimited": "Nieograniczone", + "UnmappedFilesOnly": "Tylko niezamapowane pliki", + "UnmappedFolders": "Niezamapowane foldery", + "UnmonitorDeletedEpisodes": "Przestań monitorować usunięte odcinki", + "UnmonitorDeletedEpisodesHelpText": "Odcinki usunięte z dysku są automatycznie przestawiane na niemonitorowane w {appName}", + "UnmonitorSelected": "Przestań monitorować zaznaczone", + "UnmonitorSpecialEpisodes": "Przestań monitorować odcinki specjalne", + "UnmonitorSpecialsEpisodesDescription": "Przestań monitorować wszystkie odcinki specjalne bez zmiany statusu monitorowania pozostałych odcinków", + "Unmonitored": "Niemonitorowane", + "UnmonitoredOnly": "Tylko niemonitorowane", + "UnsavedChanges": "Niezapisane zmiany", + "UnselectAll": "Odznacz wszystko", + "Upcoming": "Nadchodzące", + "UpcomingSeriesDescription": "Serial został zapowiedziany, ale nie ma jeszcze dokładnej daty emisji", + "UpdateAll": "Aktualizuj wszystko", + "UpdateAppDirectlyLoadError": "Nie można zaktualizować {appName} bezpośrednio,", + "UpdateAutomaticallyHelpText": "Automatycznie pobieraj i instaluj aktualizacje. Nadal będzie można instalować z System: Aktualizacje", + "UpdateAvailableHealthCheckMessage": "Dostępna jest nowa aktualizacja: {version}", + "UpdateFiltered": "Aktualizuj przefiltrowane", "UpdateMechanismHelpText": "Użyj wbudowanego aktualizatora {appName} lub skryptu", + "UpdateMonitoring": "Aktualizuj monitorowanie", + "UpdatePath": "Ścieżka aktualizacji", + "UpdateScriptPathHelpText": "Ścieżka do niestandardowego skryptu, który przyjmuje rozpakowany pakiet aktualizacji i obsługuje dalszą część procesu aktualizacji", + "UpdateSelected": "Aktualizuj zaznaczone", + "UpdateSeriesPath": "Aktualizuj ścieżkę serialu", + "UpdateStartupNotWritableHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ folder startowy '{startupFolder}' nie ma praw zapisu dla użytkownika '{userName}'.", + "UpdateStartupTranslocationHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ folder startowy '{startupFolder}' znajduje się w folderze App Translocation.", + "UpdateUiNotWritableHealthCheckMessage": "Nie można zainstalować aktualizacji, ponieważ folder UI '{uiFolder}' nie ma praw zapisu dla użytkownika '{userName}'.", + "UpdaterLogFiles": "Pliki logów aktualizatora", + "Updates": "Aktualizacje", + "UpgradeUntil": "Aktualizuj do", + "UpgradeUntilCustomFormatScore": "Aktualizuj do wyniku formatu niestandardowego", + "UpgradeUntilCustomFormatScoreEpisodeHelpText": "Gdy próg jakości zostanie osiągnięty lub przekroczony i osiągnięty zostanie ten wynik formatu niestandardowego, {appName} przestanie pobierać wydania odcinków", + "UpgradeUntilEpisodeHelpText": "Po osiągnięciu tej jakości {appName} przestanie pobierać odcinki po osiągnięciu lub przekroczeniu progu wyniku formatu niestandardowego", + "UpgradeUntilThisQualityIsMetOrExceeded": "Aktualizuj, aż ta jakość zostanie osiągnięta lub przekroczona", + "UpgradesAllowed": "Aktualizacje dozwolone", + "UpgradesAllowedHelpText": "Jeśli wyłączone, jakości nie będą aktualizowane", + "Uppercase": "Wielkie litery", + "Uptime": "Czas działania", + "UrlBase": "Baza URL", + "UrlBaseHelpText": "Dla obsługi reverse proxy, domyślnie puste", + "UseHardlinksInsteadOfCopy": "Używaj hardlinków zamiast kopiowania", + "UseProxy": "Używaj proxy", + "UseSeasonFolder": "Używaj folderu sezonu", + "UseSeasonFolderHelpText": "Sortuj odcinki do folderów sezonów", + "UseSsl": "Używaj SSL", + "Usenet": "Usenet", + "UsenetBlackhole": "Czarna dziura Usenet", + "UsenetBlackholeNzbFolder": "Folder NZB", + "UsenetDelay": "Opóźnienie Usenet", + "UsenetDelayHelpText": "Opóźnienie w minutach przed pobraniem wydania z Usenetu", + "UsenetDelayTime": "Opóźnienie Usenet: {usenetDelay}", + "UsenetDisabled": "Usenet wyłączony", + "UserInvokedSearch": "Wyszukiwanie wywołane przez użytkownika", + "UserRejectedExtensions": "Dodatkowe odrzucone rozszerzenia plików", + "UserRejectedExtensionsHelpText": "Lista rozszerzeń plików oddzielona przecinkami, które mają kończyć się błędem (Fail Downloads musi też być włączone dla indekswera)", + "UserRejectedExtensionsTextsExamples": "Przykłady: '.ext, .xyz' lub 'ext,xyz'", + "Username": "Nazwa użytkownika", + "UtcAirDate": "Data emisji UTC", + "Version": "Wersja", + "VersionNumber": "Wersja {version}", + "VideoCodec": "Kodek wideo", + "VideoDynamicRange": "Zakres dynamiczny wideo", + "View": "Widok", + "ViewSeriesOnTvdb": "Zobacz {title} w TVDB", + "VisitTheWikiForMoreDetails": "Odwiedź wiki, aby poznać szczegóły: ", + "WaitingToImport": "Oczekiwanie na import", + "WaitingToProcess": "Oczekiwanie na przetworzenie", + "WantMoreControlAddACustomFormat": "Chcesz większej kontroli nad preferowanymi pobraniami? Dodaj [Format niestandardowy](/settings/customformats)", + "Wanted": "Poszukiwane", + "Warn": "Ostrzeż", + "Warning": "Ostrzeżenie", + "Wednesday": "Środa", + "Week": "Tydzień", + "WeekColumnHeader": "Nagłówek kolumny tygodnia", + "WeekColumnHeaderHelpText": "Wyświetlane nad każdą kolumną, gdy tydzień jest aktywnym widokiem", + "WhatsNew": "Co nowego?", + "WhyCantIFindMyShow": "Dlaczego nie mogę znaleźć mojego serialu?", + "Wiki": "Wiki", + "WithFiles": "Z plikami", + "WouldYouLikeToRestoreBackup": "Czy chcesz przywrócić kopię zapasową '{name}'?", + "XmlRpcPath": "Ścieżka XML RPC", + "Year": "Rok", + "Yes": "Tak", + "YesCancel": "Tak, anuluj", + "Yesterday": "Wczoraj", "YesterdayAt": "Wczoraj o {time}" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index a4a29f7b8..872d10362 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -13,6 +13,7 @@ "AddConditionError": "Não foi possível adicionar uma nova condição, tente novamente.", "AddConditionImplementation": "Adicionar condição - {implementationName}", "AddConnection": "Adicionar conexão", + "AddConnectionError": "Não foi possível adicionar uma nova conexão. Tente novamente.", "AddConnectionImplementation": "Adicionar conexão - {implementationName}", "AddCustomFilter": "Adicionar filtro personalizado", "AddCustomFormat": "Adicionar formato personalizado", @@ -39,9 +40,9 @@ "AddNewRestriction": "Adicionar nova restrição", "AddNewSeries": "Adicionar nova série", "AddNewSeriesError": "Falha ao carregar os resultados da pesquisa. Tente novamente.", - "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que você deseja adicionar.", + "AddNewSeriesHelpText": "Comece a digitar o nome da série que você deseja adicionar, é simples assim.", "AddNewSeriesRootFolderHelpText": "A subpasta \"{folder}\" será criada automaticamente", - "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos corte não atingido", + "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos limites não foram atingidos", "AddNewSeriesSearchForMissingEpisodes": "Iniciar a busca por episódios ausentes", "AddQualityProfile": "Adicionar perfil de qualidade", "AddQualityProfileError": "Não foi possível adicionar um novo perfil de qualidade. Tente novamente.", @@ -56,11 +57,16 @@ "AddedDate": "Adicionado: {date}", "AddedToDownloadQueue": "Adicionado à fila de download", "AddingTag": "Adicionar etiqueta", - "AfterManualRefresh": "Após a Atualização Manual", + "AdvancedSettings": "Configurações avançadas", + "AfterManualRefresh": "Após a atualização manual", "Age": "Tempo de vida", "AgeWhenGrabbed": "Tempo de vida (quando obtido)", "Agenda": "Programação", "AirDate": "Data de exibição", + "AirDateGracePeriod": "Período de Carência para Data de Exibição", + "AirDateGracePeriodHelpText": "Valores negativos permitem baixar antes da data de exibição, valores positivos evitam baixar após a data de exibição.", + "AirDateRestriction": "Rejeitar Lançamentos Não Exibidos", + "AirDateRestrictionHelpText": "Impede {appName} baixe lançamentos que contenham episódios que ainda não foram exibidos.", "Airs": "Vai ao ar em", "AirsDateAtTimeOn": "{date} às {time} em {networkLabel}", "AirsTbaOn": "A ser anunciado em {networkLabel}", @@ -131,16 +137,19 @@ "AutoTaggingSpecificationMaximumYear": "Ano máximo", "AutoTaggingSpecificationMinimumYear": "Ano mínimo", "AutoTaggingSpecificationNetwork": "Rede(s)", + "AutoTaggingSpecificationOriginalCountry": "País", "AutoTaggingSpecificationOriginalLanguage": "Idioma", "AutoTaggingSpecificationQualityProfile": "Perfil de qualidade", "AutoTaggingSpecificationRootFolder": "Pasta raiz", "AutoTaggingSpecificationSeriesType": "Tipo de série", - "AutoTaggingSpecificationStatus": "Status", + "AutoTaggingSpecificationStatus": "Estado", "AutoTaggingSpecificationTag": "Etiqueta", "Automatic": "Automático", "AutomaticAdd": "Adição automática", "AutomaticSearch": "Pesquisa automática", "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", + "AverageSize": "Tamanho Médio", + "AverageSizePerEpisode": "Tamanho Médio por Episódio", "Backup": "Backup", "BackupFolderHelpText": "Os caminhos relativos estarão no diretório AppData do {appName}", "BackupIntervalHelpText": "Intervalo entre backups automáticos", @@ -166,11 +175,13 @@ "BlocklistRelease": "Bloquear lançamento", "BlocklistReleaseHelpText": "Impede que este lançamento seja baixado novamente pelo {appName} via RSS ou Pesquisa automática", "BlocklistReleases": "Bloquear lançamentos", + "Blocklisted": "Bloqueados", + "BlocklistedAt": "Bloqueado em {date}", "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": "Integrado", + "BuiltIn": "Incorporado", "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", @@ -228,7 +239,7 @@ "CloneAutoTag": "Clonar etiqueta automática", "CloneCondition": "Clonar condição", "CloneCustomFormat": "Clonar formato personalizado", - "CloneImportList": "Clonar Lista de Importação", + "CloneImportList": "Clonar lista de importação", "CloneIndexer": "Clonar indexador", "CloneProfile": "Clonar perfil", "Close": "Fechar", @@ -249,8 +260,8 @@ "ConnectSettingsSummary": "Notificações, conexões com servidores/reprodutores de mídia e scripts personalizados", "Connection": "Conexão", "ConnectionLost": "Conexão perdida", - "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente, ou você pode clicar em Recarregar abaixo.", - "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisa ser recarregado para restaurar a funcionalidade.", + "ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.", + "ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.", "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo ao URL {connectionName}, como {url}", "Connections": "Conexões", "ConnectionsLoadError": "Não foi possível carregar Conexões", @@ -356,6 +367,7 @@ "DeleteEpisodeFromDisk": "Excluir episódio do disco", "DeleteEpisodesFiles": "Excluir {episodeFileCount} arquivos de episódios", "DeleteEpisodesFilesHelpText": "Excluir os arquivos de episódios e a pasta da série", + "DeleteFiles": "Excluir Arquivos", "DeleteImportList": "Excluir lista de importação", "DeleteImportListExclusion": "Excluir exclusão da lista de importação", "DeleteImportListExclusionMessageText": "Tem certeza de que deseja remover esta exclusão da lista de importação?", @@ -370,19 +382,21 @@ "DeleteReleaseProfileMessageText": "Tem certeza de que deseja excluir o perfil de lançamento \"{name}\"?", "DeleteRemotePathMapping": "Excluir mapeamento de caminho remoto", "DeleteRemotePathMappingMessageText": "Tem certeza de que deseja excluir este mapeamento de caminho remoto?", - "DeleteSelected": "Excluir selecionado(s)", + "DeleteSelected": "Excluir selecionado", "DeleteSelectedCustomFormats": "Excluir formato(s) personalizado(s)", "DeleteSelectedCustomFormatsMessageText": "Tem certeza que deseja excluir o(s) {count} formato(s) personalizado(s) selecionado(s)?", "DeleteSelectedDownloadClients": "Excluir cliente(s) de download", "DeleteSelectedDownloadClientsMessageText": "Tem certeza de que deseja excluir o(s) {count} cliente(s) de download selecionado(s)?", "DeleteSelectedEpisodeFiles": "Excluir arquivos de episódios selecionados", "DeleteSelectedEpisodeFilesHelpText": "Tem certeza de que deseja excluir os arquivos de episódios selecionados?", - "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja remover as exclusões de lista de importação selecionadas?", + "DeleteSelectedImportListExclusionsMessageText": "Tem certeza de que deseja remover as exclusões selecionadas da lista de importação?", "DeleteSelectedImportLists": "Excluir lista(s) de importação", "DeleteSelectedImportListsMessageText": "Tem certeza de que deseja excluir a(s) {count} lista(s) de importação selecionada(s)?", "DeleteSelectedIndexers": "Excluir indexador(es)", "DeleteSelectedIndexersMessageText": "Tem certeza de que deseja excluir o(s) {count} indexador(es) selecionado(s)?", "DeleteSelectedSeries": "Excluir séries selecionadas", + "DeleteSelectedSeriesFiles": "Excluir Arquivos Selecionados da Série", + "DeleteSeriesFilesConfirmation": "Você tem certeza de que quer excluir todos os arquivos de episódios rastreados para {count} das 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 as {count} séries selecionadas?", @@ -468,7 +482,7 @@ "DownloadClientFreeboxSettingsPortHelpText": "Porta usada para acessar a interface do Freebox, o padrão é \"{port}\"", "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})", "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\".", - "DownloadClientItemErrorMessage": "{clientName} está relatando um erro: {message}", + "DownloadClientItemErrorMessage": "O {clientName} está relatando um erro: {message}", "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", "DownloadClientNzbgetValidationKeepHistoryOverMax": "A configuração KeepHistory do NzbGet deve ser menor que 25.000", @@ -556,6 +570,7 @@ "DownloadClientTriblerSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Tribler", "DownloadClientTriblerSettingsSafeSeeding": "Semeadura Segura", "DownloadClientTriblerSettingsSafeSeedingHelpText": "Quando ativado, apenas semeia por meio de proxies.", + "DownloadClientUTorrentProviderMessage": "O uTorrent tem um histórico de inclusão de criptomineradores, malware e anúncios. Recomendamos fortemente que você escolha um cliente diferente.", "DownloadClientUTorrentTorrentStateError": "O uTorrent está relatando um erro", "DownloadClientUnavailable": "Cliente de download indisponível", "DownloadClientValidationApiKeyIncorrect": "Chave da API incorreta", @@ -693,6 +708,7 @@ "Events": "Eventos", "Example": "Exemplo", "Exception": "Exceção", + "ExcludeSpecials": "Excluir Especiais", "ExcludeUnknownSeriesItems": "Excluir Itens de Séries Desconhecidas", "ExcludedReleaseProfile": "Perfil de Lançamento Excluído", "ExcludedReleaseProfiles": "Perfis de Lançamentos Excluídos", @@ -710,7 +726,7 @@ "Failed": "Falhou", "FailedAt": "Falha em: {date}", "FailedToFetchSettings": "Falha ao obter configurações", - "FailedToFetchUpdates": "Falha ao obter atualizações", + "FailedToFetchUpdates": "Falha ao buscar atualizações", "FailedToLoadCustomFiltersFromApi": "Falha ao carregar filtros personalizados da API", "FailedToLoadQualityProfilesFromApi": "Falha ao carregar perfis de qualidade da API", "FailedToLoadSeriesFromApi": "Falha ao carregar a série da API", @@ -731,7 +747,7 @@ "FileManagement": "Gerenciamento de arquivos", "FileNameTokens": "Tokens de nome de arquivo", "FileNames": "Nomes de arquivos", - "FileSize": "Tamanho do Arquivo", + "FileSize": "Tamanho do arquivo", "Filename": "Nome do arquivo", "Files": "Arquivos", "Filter": "Filtro", @@ -782,6 +798,7 @@ "Formats": "Formatos", "Forums": "Fóruns", "FreeSpace": "Espaço livre", + "Friday": "Sexta-feira", "From": "De", "FullColorEvents": "Eventos em cores", "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", @@ -806,7 +823,7 @@ "Health": "Integridade", "HealthIssue": "1 problema de saúde", "HealthIssues": "{count} problemas de saúde", - "HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.", + "HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha, ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.", "Here": "aqui", "HiddenClickToShow": "Oculto, clique para mostrar", "HideAdvanced": "Ocultar opções avançadas", @@ -1031,7 +1048,7 @@ "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 da 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 séries padrão/diárias", "IndexerSettingsCookie": "Cookie", @@ -1094,6 +1111,7 @@ "InstanceName": "Nome da instância", "InstanceNameHelpText": "Nome da instância na aba e para o nome do aplicativo Syslog", "InteractiveImport": "Importação interativa", + "InteractiveImportDuplicateEpisodes": "Um ou mais episódios foram atribuídos a vários arquivos", "InteractiveImportLoadError": "Não foi possível carregar itens de importação manual", "InteractiveImportMultipleQueueItems": "Múltiplos Itens da Fila", "InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado", @@ -1168,6 +1186,7 @@ "Logs": "Logs", "LongDateFormat": "Formato longo de data", "Lowercase": "Minúsculas", + "MainNavigation": "Navegação Principal", "MaintenanceRelease": "Versão de manutenção: correções de bugs e outras melhorias. Consulte o Histórico de commits do Github para saber mais", "ManageClients": "Gerenciar clientes", "ManageCustomFormats": "Gerenciar formatos personalizados", @@ -1376,7 +1395,7 @@ "NotificationTriggersHelpText": "Selecione quais eventos devem acionar esta notificação", "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.", - "NotificationsAppriseSettingsIncludePoster": "Incluir Pôster", + "NotificationsAppriseSettingsIncludePoster": "Incluir pôster", "NotificationsAppriseSettingsIncludePosterHelpText": "Incluir pôster na mensagem", "NotificationsAppriseSettingsNotificationType": "Tipo de notificação do Apprise", "NotificationsAppriseSettingsPasswordHelpText": "Senha de autenticação HTTP básica", @@ -1477,9 +1496,9 @@ "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", "NotificationsPushcutSettingsIncludePoster": "Incluir pôster", - "NotificationsPushcutSettingsIncludePosterHelpText": "Incluir pôster com notificação", + "NotificationsPushcutSettingsIncludePosterHelpText": "Incluir pôster na notificação", "NotificationsPushcutSettingsMetadataLinks": "Links de metadados", - "NotificationsPushcutSettingsMetadataLinksHelpText": "Adicionar links para os metadados da série ao enviar notificações", + "NotificationsPushcutSettingsMetadataLinksHelpText": "Adicionar links aos metadados da série ao enviar notificações", "NotificationsPushcutSettingsNotificationName": "Nome da Notificação", "NotificationsPushcutSettingsNotificationNameHelpText": "Nome da notificação na aba Notificações do aplicativo Pushcut", "NotificationsPushcutSettingsTimeSensitive": "Urgente", @@ -1492,7 +1511,7 @@ "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", - "NotificationsPushoverSettingsTtl": "Tempo para Viver", + "NotificationsPushoverSettingsTtl": "Tempo de vida", "NotificationsPushoverSettingsTtlHelpText": "Tempo em segundos antes da mensagem expirar. Defina como 0 para duração ilimitada", "NotificationsPushoverSettingsUserKey": "Chave do usuário", "NotificationsSendGridSettingsApiKeyHelpText": "A chave da API gerada pelo SendGrid", @@ -1573,8 +1592,8 @@ "OnApplicationUpdate": "Na Atualização do Aplicativo", "OnEpisodeFileDelete": "Ao Excluir o Arquivo do Episódio", "OnEpisodeFileDeleteForUpgrade": "No Arquivo do Episódio Excluir para Atualização", - "OnFileImport": "Ao Importar o Arquivo", - "OnFileUpgrade": "Ao Atualizar o Arquivo", + "OnFileImport": "Ao importar o arquivo", + "OnFileUpgrade": "Ao atualizar o arquivo", "OnGrab": "Ao obter", "OnHealthIssue": "Ao Problema de Saúde", "OnHealthRestored": "Com a Saúde Restaurada", @@ -1607,6 +1626,7 @@ "OrganizeSelectedSeriesModalConfirmation": "Tem certeza de que deseja organizar todos os arquivos da {count} série selecionada?", "OrganizeSelectedSeriesModalHeader": "Organizar Séries Selecionadas", "Original": "Original", + "OriginalCountry": "País Original", "OriginalLanguage": "Idioma Original", "Other": "Outro", "OutputPath": "Caminho de saída", @@ -1620,6 +1640,11 @@ "OverviewOptions": "Opções da visão geral", "PackageVersion": "Versão do pacote", "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "PagerGoToFirstPage": "Ir para a primeira página", + "PagerGoToLastPage": "Ir para a última página", + "PagerGoToNextPage": "Ir para a próxima página", + "PagerGoToPage": "Ir para a página {page} de {totalPages}", + "PagerGoToPreviousPage": "Ir para a página anterior", "Parse": "Analisar", "ParseModalErrorParsing": "Erro ao analisar, tente novamente.", "ParseModalHelpText": "Insira um título de lançamento na entrada acima", @@ -1640,7 +1665,7 @@ "Permissions": "Permissões", "Port": "Porta", "PortNumber": "Número da Porta", - "PostImportCategory": "Categoria Pós-Importação", + "PostImportCategory": "Categoria pós-importação", "PosterOptions": "Opções do pôster", "PosterSize": "Tamanho do Pôster", "Posters": "Pôsteres", @@ -1677,7 +1702,7 @@ "ProxyResolveIpHealthCheckMessage": "Falha ao resolver o endereço IP do host de proxy configurado {proxyHostName}", "ProxyType": "Tipo de Proxy", "ProxyUsernameHelpText": "Você só precisa digitar um nome de usuário e senha se for necessário. Caso contrário, deixe-os em branco.", - "PublishedDate": "Data de Publicação", + "PublishedDate": "Data de publicação", "Qualities": "Qualidades", "QualitiesHelpText": "As qualidades mais altas na lista são mais preferidas (mesmo quando não são marcadas). As qualidades dentro do mesmo grupo são iguais. Somente qualidades marcadas são desejadas", "QualitiesLoadError": "Não foi possível carregar qualidades", @@ -1688,6 +1713,9 @@ "QualityDefinitionsSizeNotice": "As restrições de tamanho foram transferidas para Perfis de Qualidade", "QualityProfile": "Perfil de qualidade", "QualityProfileInUseSeriesListCollection": "Não é possível excluir um perfil de qualidade anexado a uma série, lista ou coleção", + "QualityProfileUsage": "Uso do Perfil de Qualidade", + "QualityProfileUsedInCountImportLists": "Usado em {count} listas de importação", + "QualityProfileUsedInCountSeries": "Usado em {count} séries", "QualityProfiles": "Perfis de Qualidade", "QualityProfilesLoadError": "Não é possível carregar perfis de qualidade", "QualitySettings": "Configurações de Qualidade", @@ -1706,7 +1734,7 @@ "ReadTheWikiForMoreInformation": "Leia o Wiki para mais informações", "Real": "Real", "Reason": "Razão", - "RecentChanges": "Mudanças Recentes", + "RecentChanges": "Mudanças recentes", "RecentFolders": "Pastas Recentes", "RecycleBinUnableToWriteHealthCheckMessage": "Não é possível gravar na pasta da lixeira configurada: {path}. Certifique-se de que este caminho exista e seja gravável pelo usuário executando o {appName}", "RecyclingBin": "Lixeira", @@ -1729,14 +1757,14 @@ "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.`).", "ReleaseGroups": "Grupos do Lançamento", "ReleaseHash": "Hash do Lançamento", - "ReleaseProfile": "Perfil de Lançamento", + "ReleaseProfile": "Perfil de lançamento", "ReleaseProfileExcludedTagSeriesHelpText": "Os perfis de lançamento não se aplicarão a séries com pelo menos uma etiqueta correspondente.", "ReleaseProfileIndexerHelpText": "Especifique a qual indexador o perfil se aplica", "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.", "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", - "ReleasePush": "Impulsionar Lançamento", + "ReleasePush": "Impulsionar lançamento", "ReleaseRejected": "Lançamento Rejeitado", "ReleaseSceneIndicatorAssumingScene": "Assumindo a Numeração da Scene.", "ReleaseSceneIndicatorAssumingTvdb": "Assumindo a numeração TVDB.", @@ -1744,7 +1772,7 @@ "ReleaseSceneIndicatorSourceMessage": "Existem lançamentos de {message} com numeração ambígua, incapaz de identificar o episódio de forma confiável.", "ReleaseSceneIndicatorUnknownMessage": "A numeração varia para este episódio e o lançamento não corresponde a nenhum mapeamento conhecido.", "ReleaseSceneIndicatorUnknownSeries": "Episódio ou série desconhecida.", - "ReleaseSource": "Fonte do Lançamento", + "ReleaseSource": "Origem do lançamento", "ReleaseTitle": "Título do Lançamento", "ReleaseType": "Tipo de Lançamento", "Reload": "Recarregar", @@ -1788,7 +1816,7 @@ "RemoveQueueItemRemovalMethod": "Método de Remoção", "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.", "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remover do Cliente de Download' removerá os downloads e os arquivos do cliente de download.", - "RemoveRootFolder": "Remover Pasta Raiz", + "RemoveRootFolder": "Remover pasta raiz", "RemoveRootFolderWithSeriesMessageText": "Tem certeza de que deseja remover a pasta raiz '{path}'? Arquivos e pastas não serão excluídos do disco e as séries nesta pasta raiz não serão removidas de {appName}.", "RemoveSelected": "Remover Selecionado", "RemoveSelectedBlocklistMessageText": "Tem certeza de que deseja remover os itens selecionados da lista de bloqueio?", @@ -1822,7 +1850,7 @@ "RescanSeriesFolderAfterRefresh": "Verificar novamente a pasta da série após a atualização", "Reset": "Redefinir", "ResetAPIKey": "Redefinir chave de API", - "ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave de API?", + "ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave da API?", "ResetDefinitionTitlesHelpText": "Redefinir títulos de definição e valores", "ResetDefinitions": "Redefinir definições", "ResetQualityDefinitions": "Redefinir Definições de Qualidade", @@ -1830,7 +1858,7 @@ "Restart": "Reiniciar", "RestartLater": "Reiniciarei mais tarde", "RestartNow": "Reiniciar Agora", - "RestartReloadNote": "Observação: o {appName} reiniciará automaticamente e recarregará a IU durante o processo de restauração.", + "RestartReloadNote": "Observação: o {appName} reiniciará automaticamente e recarregará a interface durante o processo de restauração.", "RestartRequiredHelpTextWarning": "Requer reinicialização para entrar em vigor", "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.", @@ -1848,7 +1876,7 @@ "RootFolderMultipleEmptyHealthCheckMessage": "Múltiplas pastas raiz estão vazias: {rootFolderPaths}", "RootFolderMultipleMissingHealthCheckMessage": "Faltam várias pastas raiz: {rootFolderPaths}", "RootFolderPath": "Caminho da Pasta Raiz", - "RootFolderSelectFreeSpace": "{freeSpace} Livre", + "RootFolderSelectFreeSpace": "{freeSpace} livre(s)", "RootFolders": "Pastas Raiz", "RootFoldersLoadError": "Não foi possível carregar as pastas raiz", "Rss": "RSS", @@ -1857,7 +1885,9 @@ "RssSyncInterval": "Intervalo da Sincronização RSS", "RssSyncIntervalHelpText": "Intervalo em minutos. Defina como zero para desativar (isso interromperá todas as capturas de lançamentos automáticas)", "RssSyncIntervalHelpTextWarning": "Isso se aplica a todos os indexadores, siga as regras estabelecidas por eles", + "Run": "Executar", "Runtime": "Duração", + "Saturday": "Sábado", "Save": "Salvar", "SaveChanges": "Salvar Mudanças", "SaveSettings": "Salvar configurações", @@ -1906,6 +1936,8 @@ "SeasonPremiere": "Estreia da Temporada", "SeasonPremieresOnly": "Somente Estreias da Temporada", "Seasons": "Temporadas", + "SeasonsMonitoredAll": "Todas", + "SeasonsMonitoredNone": "Nenhuma", "SeasonsMonitoredStatus": "Temporadas monitoradas", "SecretToken": "Token Secreto", "Security": "Segurança", @@ -1944,6 +1976,7 @@ "SeriesFolderImportedTooltip": "Episódio importado da pasta da série", "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.", "SeriesID": "ID da Série", + "SeriesInImportListExclusions": "Séries estão nas Exclusões da Lista de Importação", "SeriesIndexFooterContinuing": "Continuando (todos os episódios baixados)", "SeriesIndexFooterDownloading": "Baixando (um ou mais episódios)", "SeriesIndexFooterEnded": "Terminado (todos os episódios baixados)", @@ -2003,7 +2036,7 @@ "SizeLimit": "Limite de Tamanho", "SizeOnDisk": "Tamanho no disco", "SkipFreeSpaceCheck": "Ignorar verificação de espaço livre", - "SkipFreeSpaceCheckHelpText": "Usar quando {appName} não consegue detectar espaço livre em sua pasta raiz", + "SkipFreeSpaceCheckHelpText": "Usar quando o {appName} não conseguir detectar espaço livre em sua pasta raiz", "SkipRedownload": "Ignorar o Redownload", "SkipRedownloadHelpText": "Impede que o {appName} tente baixar uma versão alternativa para este item", "Small": "Pequeno", @@ -2053,6 +2086,7 @@ "SupportedListsMoreInfo": "Para obter mais informações sobre as listas individuais, clique nos botões de mais informações.", "SupportedListsSeries": "O {appName} oferece suporte a várias listas para importar séries para o banco de dados.", "System": "Sistema", + "SystemDefault": "Padrão do sistema", "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", "Table": "Tabela", "TableColumns": "Colunas", @@ -2083,9 +2117,11 @@ "Theme": "Tema", "ThemeHelpText": "Alterar o tema da interface do usuário do aplicativo, o tema 'Auto' usará o tema do sistema operacional para definir o modo Claro ou Escuro. Inspirado por Theme.Park", "Threshold": "Limite", + "Thursday": "Quinta-feira", "Time": "Horário", "TimeFormat": "Formato da Hora", "TimeLeft": "Tempo Restante", + "TimeZone": "Fuso Horário", "Title": "Título", "Titles": "Título", "Today": "Hoje", @@ -2114,6 +2150,7 @@ "TotalSpace": "Espaço Total", "Trace": "Traço", "True": "Verdadeiro", + "Tuesday": "Terça-feira", "TvdbId": "TVDB ID", "TvdbIdExcludeHelpText": "A ID TVDB da série a ser excluída", "Twitter": "Twitter", @@ -2126,15 +2163,15 @@ "UiSettingsLoadError": "Não foi possível carregar as configurações da UI", "UiSettingsSummary": "Opções de calendário, data e cores para daltônicos", "Umask": "Desmascarar", - "Umask750Description": "{octal} - gravação do proprietário, leitura do grupo", - "Umask755Description": "{octal} - Escrita do proprietário, todos os outros lêem", - "Umask770Description": "{octal} - proprietário e gravação do grupo", - "Umask775Description": "{octal} - gravação do proprietário e do grupo, leitura de outros", - "Umask777Description": "{octal} - Todo mundo escreve", + "Umask750Description": "{octal} - Proprietário pode gravar, grupo pode ler", + "Umask755Description": "{octal} - Proprietário pode gravar, todos os outros podem ler", + "Umask770Description": "{octal} - Proprietário e grupo podem gravar", + "Umask775Description": "{octal} - Proprietário e grupo podem gravar, outros podem ler", + "Umask777Description": "{octal} - Todos podem gravar", "UnableToImportAutomatically": "Não foi possível importar automaticamente", "UnableToLoadAutoTagging": "Não foi possível carregar as etiquetas automáticas", "UnableToLoadBackups": "Não foi possível carregar os backups", - "UnableToUpdateSonarrDirectly": "Incapaz de atualizar o {appName} diretamente,", + "UnableToUpdateSonarrDirectly": "Não foi possível atualizar o {appName} diretamente,", "Unavailable": "Indisponível", "Underscore": "Sublinhar", "Ungroup": "Desagrupar", @@ -2157,13 +2194,13 @@ "Upcoming": "Por vir", "UpcomingSeriesDescription": "A série foi anunciada, mas ainda não há data exata para ir ao ar", "UpdateAll": "Atualizar Tudo", - "UpdateAppDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,", + "UpdateAppDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,", "UpdateAutomaticallyHelpText": "Baixe e instale atualizações automaticamente. Você ainda poderá instalar a partir do Sistema: Atualizações", "UpdateAvailableHealthCheckMessage": "Nova atualização está disponível: {version}", "UpdateFiltered": "Atualização Filtrada", "UpdateMechanismHelpText": "Usar o atualizador integrado do {appName} ou um script", "UpdateMonitoring": "Atualizar Monitoramento", - "UpdatePath": "Caminho da Atualização", + "UpdatePath": "Caminho da atualização", "UpdateScriptPathHelpText": "Caminho para um script personalizado que usa um pacote de atualização extraído e lida com o restante do processo de atualização", "UpdateSelected": "Atualizar Selecionado", "UpdateSeriesPath": "Atualizar Caminho da Série", @@ -2195,7 +2232,7 @@ "UsenetDelayHelpText": "Atraso em minutos para esperar antes de pegar um lançamento da Usenet", "UsenetDelayTime": "Atraso da Usenet: {usenetDelay}", "UsenetDisabled": "Usenet Desabilitada", - "UserInvokedSearch": "Pesquisa Invocada pelo Usuário", + "UserInvokedSearch": "Pesquisa iniciada pelo usuário", "UserRejectedExtensions": "Extensões de Arquivos Rejeitadas Adicionais", "UserRejectedExtensionsHelpText": "Lista separada por vírgulas de extensões de arquivos para falhar (Falha em downloads também precisa ser habilitado por indexador)", "UserRejectedExtensionsTextsExamples": "Exemplos: '.ext, .xyz' or 'ext,xyz'", @@ -2206,13 +2243,15 @@ "VideoCodec": "Codec de Vídeo", "VideoDynamicRange": "Faixa Dinâmica de Vídeo", "View": "Exibir", + "ViewSeriesOnTvdb": "Exibir {title} no TVDB", "VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ", "WaitingToImport": "Aguardando para Importar", "WaitingToProcess": "Aguardando para Processar", "WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)", "Wanted": "Procurado", "Warn": "Alerta", - "Warning": "Cuidado", + "Warning": "Aviso", + "Wednesday": "Quarta-feira", "Week": "Semana", "WeekColumnHeader": "Cabeçalho da Coluna da Semana", "WeekColumnHeaderHelpText": "Mostrado acima de cada coluna quando a semana é a exibição ativa", diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 9d2846cce..95fccbad9 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -24,6 +24,7 @@ "AddIndexerImplementation": "Adăugați Indexator - {implementationName}", "AddList": "Adaugă listă", "AddListError": "Nu se poate adăuga o nouă listă, vă rugăm să încercați din nou.", + "AddListExclusion": "Adăugați excluderea listei", "AddListExclusionError": "Imposibil de adăugat o nouă listă de excludere, încercați din nou.", "AddNew": "Adaugă nou", "AddNewRestriction": "Adăugați o restricție nouă", @@ -39,6 +40,7 @@ "AfterManualRefresh": "După reîmprospătarea manuală", "Age": "Vechime", "AirDate": "Data de difuzare", + "AirDateRestriction": "Respinge lansările ne difuzate", "All": "Toate", "AllResultsAreHiddenByTheAppliedFilter": "Toate rezultatele sunt ascunse de filtrul aplicat", "AllTitles": "Toate titlurile", @@ -49,6 +51,7 @@ "AnalyticsEnabledHelpText": "Trimiteți informații anonime privind utilizarea și erorile către serverele {appName}. Aceasta include informații despre browserul dvs., ce pagini WebUI {appName} utilizați, raportarea erorilor, precum și sistemul de operare și versiunea de execuție. Vom folosi aceste informații pentru a acorda prioritate caracteristicilor și remedierilor de erori.", "Any": "Oricare", "ApiKey": "Cheie API", + "ApiKeyValidationHealthCheckMessage": "Te rugăm să actualizezi cheia API astfel încât să aibă cel puțin {length} caractere. Poți face acest lucru din setări sau din fișierul de configurare", "AppDataDirectory": "Directorul AppData", "AppDataLocationHealthCheckMessage": "Pentru a preveni ștergerea AppData, update-ul nu este posibil", "AppUpdated": "{appName} actualizat", @@ -71,14 +74,17 @@ "AuthenticationRequired": "Autentificare necesara", "AuthenticationRequiredPasswordHelpTextWarning": "Introduceți o parolă nouă", "AuthenticationRequiredUsernameHelpTextWarning": "Introduceți un nou nume de utilizator", + "AutoTaggingSpecificationOriginalCountry": "Țară", "AutomaticAdd": "Adăugare automată", "Backup": "Copie de rezervă", "BackupNow": "Fă o copie de rezervă", "Backups": "Copii de rezervă", "BeforeUpdate": "Înainte de actualizare", "BlocklistLoadError": "Imposibil de încărcat lista neagră", + "Blocklisted": "Blocat", + "BlocklistedAt": "Blocată la {date}", "Calendar": "Calendar", - "CalendarOptions": "Setări Calendar", + "CalendarOptions": "Opțiuni calendar", "Cancel": "Anulează", "CancelPendingTask": "Sigur doriți să anulați această sarcină în așteptare?", "CertificateValidationHelpText": "Modificați cât de strictă este validarea certificării HTTPS. Nu schimbați dacă nu înțelegeți riscurile.", @@ -135,6 +141,7 @@ "DownloadClientsLoadError": "Nu se pot încărca clienții de descărcare", "DownloadIgnored": "Descărcarea ignorată", "Edit": "Editează", + "Empty": "Gol", "EnableAutomaticSearch": "Activați căutarea automată", "EnableInteractiveSearch": "Activați căutarea interactivă", "Enabled": "Activat", @@ -148,8 +155,10 @@ "Events": "Evenimente", "Exception": "Excepție", "ExistingTag": "Etichetă existentă", + "ExpandAll": "Extinde tot", "ExportCustomFormat": "Exportați formatul personalizat", "Failed": "Eșuat", + "FailedAt": "Eșuat la: {date}", "False": "Fals", "FileNameTokens": "Jetoane pentru nume de fișier", "Filename": "Nume fișier", @@ -162,15 +171,21 @@ "FormatAgeMinutes": "minute", "Formats": "Formate", "FreeSpace": "Spațiu Liber", + "Friday": "Vineri", "FullColorEvents": "Evenimente pline de culoare", + "FullSeason": "Sezon full", "General": "General", "GeneralSettings": "Setări generale", "Genres": "Genuri", + "GrabbedAt": "Preluat la: {date}", + "HasMissingSeason": "Are sezon lipsă", + "HasUnmonitoredSeason": "Are sezon nemonitorizat", "Health": "Sănătate", "HiddenClickToShow": "Ascuns, faceți clic pentru afișare", "HideAdvanced": "Ascunde Avansat", "History": "Istoric", "HistoryLoadError": "Istoricul nu poate fi încărcat", + "HistoryModalHeaderSeason": "Istoric {season}", "HomePage": "Pagina principală", "Ignored": "Ignorat", "Implementation": "Implementarea", @@ -180,43 +195,87 @@ "InteractiveImportNoFilesFound": "Nu au fost găsite fișiere video în folderul selectat", "InteractiveImportNoImportMode": "Un mod de import trebuie selectat", "InteractiveImportNoQuality": "Calitatea trebuie aleasă pentru fiecare fișier selectat", + "InteractiveSearchGrabError": "NU s-a putut adăuga în coada de descărcare", "LanguagesLoadError": "Nu se pot încărca limbile", "LastDuration": "Ultima durată", + "Links": "Linkuri", "LongDateFormat": "Format de dată lungă", + "ManageEpisodes": "Gestionează episoadele", + "ManageEpisodesSeason": "Gestionează fișierele episoadelor din acest sezon", + "MonitorFirstSeason": "Primul sezon", + "MonitorLastSeason": "Ultimul sezon", + "MonitorNoNewSeasons": "Nu există sezoane noi", + "Monitored": "Monitorizat", "MoreInfo": "Mai multe informații", + "NoEpisodesInThisSeason": "Nu exista episoade în acest sezon", "NoHistoryFound": "Nu s-a găsit istoric", + "NoSeasons": "Fără sezoane", "NotificationStatusSingleClientHealthCheckMessage": "Notificări indisponibile datorită erorilor: {notificationNames}", "OnFileImport": "La import fișier", "OnFileUpgrade": "La actualizare fișier", + "OneSeason": "1 sezon", "Or": "sau", + "OriginalCountry": "Țară originală", "Parse": "Analiza", "ParseModalErrorParsing": "Eroare la analizare, încercați din nou.", "ParseModalUnableToParse": "Nu se poate analiza titlul furnizat, vă rugăm să încercați din nou.", + "PartialSeason": "Sezon parțial", "Pending": "În așteptare", "PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil", + "PreviewRenameSeason": "Previzualizează redenumirea pentru acest sezon", "QualitiesLoadError": "Nu se pot încărca calitățile", "QueueLoadError": "Nu s-a putut încărca coada de așteptare", "ReleaseProfilesLoadError": "Nu se pot încărca profilurile", "RemoveSelectedBlocklistMessageText": "Sigur doriți să eliminați elementele selectate din lista neagră?", + "RootFolderEmptyHealthCheckMessage": "Folder root gol: {rootFolderPath}", + "RootFolderMultipleEmptyHealthCheckMessage": "Mai multe foldere root sunt goale: {rootFolderPaths}", + "Saturday": "Sâmbătă", + "SearchForQuery": "Caută {query}", + "Season": "Sezon", + "SeasonCount": "Număr de sezoane", + "SeasonDetails": "Detalii sezon", + "SeasonFinale": "Finalul sezonului", + "SeasonFolder": "Folder sezon", + "SeasonFolderFormat": "Format folder sezon", + "SeasonInformation": "Informații sezon", + "SeasonNumber": "Număr sezon", + "SeasonNumberToken": "Sezon {seasonNumber}", + "SeasonPack": "Pachet sezon", + "SeasonPremiere": "Premiera sezonului", + "SeasonPremieresOnly": "Doar premierele sezonului", + "Seasons": "Sezoane", + "SeasonsMonitoredAll": "Toate", + "SeasonsMonitoredNone": "Niciunul", + "SeasonsMonitoredStatus": "Sezoane monitorizate", "SelectDownloadClientModalTitle": "{modalTitle} - Selectați clientul de descărcare", "SelectDropdown": "Selectați...", "SelectFolderModalTitle": "{modalTitle} - Selectați folder", "SelectLanguageModalTitle": "{modalTitle} - Selectează limba", + "SelectSeason": "Selectați sezonul", + "SelectSeasonModalTitle": "{modalTitle} - Selectați sezonul", "SetReleaseGroupModalTitle": "{modalTitle} - Setați grupul de lansare", "ShortDateFormat": "Format scurt de dată", "ShowAdvanced": "Arată setări avansate", "ShowRelativeDates": "Afișați datele relative", "ShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute", + "ShowSeasonCount": "Afișează numărul de sezoane", "ShownClickToHide": "Afișat, faceți clic pentru a ascunde", + "StandardEpisodeTypeFormat": "Numerele sezonului si episodului ({format})", "TablePageSize": "Mărimea Paginii", "TablePageSizeHelpText": "Numărul de articole de afișat pe fiecare pagină", + "Thursday": "Joi", "TimeFormat": "Format ora", + "TimeZone": "Fus orar", "True": "Adevărat", + "Tuesday": "Marți", "Umask": "Umask", "Unknown": "Necunoscut", "UnknownEventTooltip": "Eveniment necunoscut", "UpdateAvailableHealthCheckMessage": "O nouă versiune este disponibilă: {version}", + "UseSeasonFolder": "Folosește folderul sezonului", + "UseSeasonFolderHelpText": "Sortează episoadele în foldere de sezon", "Warning": "Avertisment", + "Wednesday": "Miercuri", "Week": "Săptămână", "WeekColumnHeader": "Antetul coloanei săptămânii", "WhatsNew": "Ce-i nou?", diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index 2157b6321..f3018eea4 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -13,6 +13,7 @@ "AddConditionError": "Невозможно добавить новое условие, попробуйте еще раз.", "AddConditionImplementation": "Добавить условие - {implementationName}", "AddConnection": "Добавить подключение", + "AddConnectionError": "Невозможно добавить новое соединение, пожалуйста, попробуйте еще раз.", "AddConnectionImplementation": "Добавить подключение - {implementationName}", "AddCustomFilter": "Добавить специальный фильтр", "AddCustomFormat": "Добавить свой формат", diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index abb8e9223..c0837363b 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -61,6 +61,10 @@ "AgeWhenGrabbed": "Вік (коли схоплено)", "Agenda": "План", "AirDate": "Дата виходу в ефір", + "AirDateGracePeriod": "Допустимий період відносно дати виходу в ефір", + "AirDateGracePeriodHelpText": "Від’ємні значення дозволяють завантаження до дати виходу в ефір, додатні — забороняють завантаження після дати виходу в ефір.", + "AirDateRestriction": "Відхиляти релізи з невипущеними епізодами", + "AirDateRestrictionHelpText": "Запобігти тому, щоб {appName} завантажував релізи, які містять епізоди, що ще не вийшли в ефір.", "Airs": "Ефіри", "AirsDateAtTimeOn": "{date} о {time} на {networkLabel}", "AirsTbaOn": "Будь ласка, оголосіть пізніше на {networkLabel}", @@ -131,6 +135,7 @@ "AutoTaggingSpecificationMaximumYear": "Максимальний рік", "AutoTaggingSpecificationMinimumYear": "Мінімальний рік", "AutoTaggingSpecificationNetwork": "Телеканал", + "AutoTaggingSpecificationOriginalCountry": "Країна", "AutoTaggingSpecificationOriginalLanguage": "Мова", "AutoTaggingSpecificationQualityProfile": "Профіль якості", "AutoTaggingSpecificationRootFolder": "Коренева тека", @@ -1588,6 +1593,7 @@ "OrganizeSelectedSeriesModalConfirmation": "Ви впевнені, що хочете упорядкувати всі файли у вибраному серіалі: {count}?", "OrganizeSelectedSeriesModalHeader": "Упорядкувати вибрані серіали", "Original": "Оригінал", + "OriginalCountry": "Країна оригіналу", "OriginalLanguage": "Мова оригіналу", "Other": "Інше", "OutputPath": "Вихідний шлях", diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index cb382378a..509d606e8 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -3,7 +3,7 @@ "Absolute": "绝对", "AbsoluteEpisodeNumber": "准确的集数", "AbsoluteEpisodeNumbers": "准确的集数", - "Actions": "动作", + "Actions": "操作", "Activity": "活动", "Add": "添加", "AddANewPath": "添加一个新的目录", @@ -13,6 +13,7 @@ "AddConditionError": "无法添加新条件,请再试一次。", "AddConditionImplementation": "添加条件 - {implementationName}", "AddConnection": "添加连接", + "AddConnectionError": "无法添加新的连接,请重试。", "AddConnectionImplementation": "添加连接- {implementationName}", "AddCustomFilter": "添加自定义过滤器", "AddCustomFormat": "添加自定义命名格式", @@ -61,6 +62,10 @@ "AgeWhenGrabbed": "年龄(抓取后)", "Agenda": "日程表", "AirDate": "播出日期", + "AirDateGracePeriod": "播出宽限期", + "AirDateGracePeriodHelpText": "负值为允许播出前抓取期限,正值为防止播出后抓取期限。", + "AirDateRestriction": "拒绝未播出版本", + "AirDateRestrictionHelpText": "避免 {appName} 抓取含有未播出集的版本。", "Airs": "播出", "AirsDateAtTimeOn": "{date} {time} 在 {networkLabel} 播出", "AirsTbaOn": "时间待公布,在 {networkLabel} 播出", @@ -131,6 +136,7 @@ "AutoTaggingSpecificationMaximumYear": "最晚年份", "AutoTaggingSpecificationMinimumYear": "最早年份", "AutoTaggingSpecificationNetwork": "网络", + "AutoTaggingSpecificationOriginalCountry": "国家", "AutoTaggingSpecificationOriginalLanguage": "语言", "AutoTaggingSpecificationQualityProfile": "质量配置", "AutoTaggingSpecificationRootFolder": "根目录文件夹", @@ -166,6 +172,8 @@ "BlocklistRelease": "发布资源黑名单", "BlocklistReleaseHelpText": "禁止 {appName}通过 RSS 或自动搜索重新下载此版本", "BlocklistReleases": "发布资源黑名单", + "Blocklisted": "已拉黑", + "BlocklistedAt": "于 {date} 拉黑", "Branch": "分支", "BranchUpdate": "更新{appName}的分支", "BranchUpdateMechanism": "外部更新机制使用的分支", @@ -253,6 +261,7 @@ "ConnectionLostToBackend": "{appName}失去了与后端的连接,需要重新加载以恢复功能。", "ConnectionSettingsUrlBaseHelpText": "向 {connectionName} URL 添加前缀,例如 {url}", "Connections": "连接", + "ConnectionsLoadError": "无法加载连接", "Continuing": "仍在继续", "ContinuingOnly": "仅包含仍在继续的", "ContinuingSeriesDescription": "预计会有更多集/下一季", @@ -355,6 +364,7 @@ "DeleteEpisodeFromDisk": "从磁盘中删除剧集", "DeleteEpisodesFiles": "删除{episodeFileCount}个剧集文件", "DeleteEpisodesFilesHelpText": "删除集文件和剧集文件夹", + "DeleteFiles": "删除文件", "DeleteImportList": "删除导入的列表", "DeleteImportListExclusion": "删除导入排除列表", "DeleteImportListExclusionMessageText": "您确认要删除此导入排除列表吗?", @@ -382,6 +392,8 @@ "DeleteSelectedIndexers": "删除索引器", "DeleteSelectedIndexersMessageText": "您确定要删除选定的 {count} 个索引器吗?", "DeleteSelectedSeries": "删除选中的剧集", + "DeleteSelectedSeriesFiles": "删除所选剧集文件", + "DeleteSeriesFilesConfirmation": "你是否确定删除 {count} 部所选剧集中所有追踪的单集?", "DeleteSeriesFolder": "删除剧集文件夹", "DeleteSeriesFolderConfirmation": "剧集文件夹 `{path}` 及所含内容将会被删除。", "DeleteSeriesFolderCountConfirmation": "你确定要删除选中的 {count} 个剧集吗?", @@ -467,6 +479,7 @@ "DownloadClientFreeboxSettingsPortHelpText": "用于访问 Freebox 接口的端口,默认为 '{port}'", "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查 “主机名”、“端口” 或 “使用 SSL” 的设置(错误: {exceptionMessage})", "DownloadClientFreeboxUnableToReachFreeboxApi": "无法访问 Freebox API。 请检查 “API 地址” 的基础地址和版本。", + "DownloadClientItemErrorMessage": "{clientName}回報錯誤:{message}", "DownloadClientNzbVortexMultipleFilesMessage": "下载包含多个文件且不在作业文件夹中:{outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "此选项至少需要 NzbGet 版本 16.0", "DownloadClientNzbgetValidationKeepHistoryOverMax": "NzbGet 设置 KeepHistory 应小于 25000", @@ -479,6 +492,8 @@ "DownloadClientPneumaticSettingsStrmFolder": "Strm 文件夹", "DownloadClientPneumaticSettingsStrmFolderHelpText": "该文件夹中的 .strm 文件将由 drone 导入", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", + "DownloadClientQbittorrentSettingsAddSeriesTags": "添加剧集标签", + "DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "向下载客户端中新添加的种子添加标签(qBittorrent 4.1.0 以上)", "DownloadClientQbittorrentSettingsContentLayout": "内容布局", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "是否使用 qBittorrent 的已配置内容布局、种子的原始布局或始终创建子文件夹(qBittorrent 4.3.2+)", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "先下载首尾文件块", @@ -545,7 +560,16 @@ "DownloadClientStatusSingleClientHealthCheckMessage": "所有下载客户端都不可用:{downloadClientNames}", "DownloadClientTransmissionSettingsDirectoryHelpText": "下载位置可选择,留空使用 Transmission 默认位置", "DownloadClientTransmissionSettingsUrlBaseHelpText": "向 {clientName} RPC URL 添加前缀,例如 {url},默认为 '{defaultUrl}'", + "DownloadClientTriblerProviderMessage": "Tribler 的支持仍在初期试验中。测试过 {clientName} 的 {clientVersionRange} 版本。", + "DownloadClientTriblerSettingsAnonymityLevel": "匿名等级", + "DownloadClientTriblerSettingsAnonymityLevelHelpText": "下载时的代理数量。设为 0 以禁用。代理会降低上传下载速度。参看 {url}", + "DownloadClientTriblerSettingsApiKeyHelpText": "triblerd.conf 中的 [api].key", + "DownloadClientTriblerSettingsDirectoryHelpText": "可选的下载位置,留空则使用 Tribler 的默认位置", + "DownloadClientTriblerSettingsSafeSeeding": "安全做种", + "DownloadClientTriblerSettingsSafeSeedingHelpText": "开启时仅通过代理做种。", + "DownloadClientUTorrentProviderMessage": "uTorrent 曾经含有挖矿行为、恶意软件和广告,我们强烈建议你选择其他客户端。", "DownloadClientUTorrentTorrentStateError": "uTorrent 报告了错误", + "DownloadClientUnavailable": "下载客户端不可用", "DownloadClientValidationApiKeyIncorrect": "API Key 不正确", "DownloadClientValidationApiKeyRequired": "需要 API Key", "DownloadClientValidationAuthenticationFailure": "认证失败", @@ -606,6 +630,8 @@ "EditSelectedSeries": "编辑所选剧集", "EditSeries": "编辑剧集", "EditSeriesModalHeader": "编辑 - {title}", + "Empty": "空", + "EmptyRootFolderTooltip": "根目录不包含任何文件或文件夹。{appName} 将不会扫描改动或建立空剧集文件夹。", "Enable": "启用", "EnableAutomaticAdd": "启用自动添加", "EnableAutomaticAddSeriesHelpText": "当通过 UI 或 {appName} 执行同步时,将剧集添加到 {appName}", @@ -649,17 +675,25 @@ "EpisodeInfo": "剧集信息", "EpisodeIsDownloading": "集正在下载", "EpisodeIsNotMonitored": "集未被监控", + "EpisodeMaybePlural": "单集", "EpisodeMissingAbsoluteNumber": "集没有准确的集数", "EpisodeMissingFromDisk": "磁盘中缺失集", + "EpisodeMonitoring": "单集追踪", "EpisodeNaming": "集命名", "EpisodeNumbers": "剧集序号", "EpisodeProgress": "剧集进度", + "EpisodeRequested": "单集已请求", "EpisodeSearchResultsLoadError": "无法加载此集的搜索结果。稍后再试", - "EpisodeTitle": "剧集标题", - "EpisodeTitleRequired": "需要集标题", + "EpisodeTitle": "单集标题", + "EpisodeTitleFootNote": "可选择控制最多字节数(包含省略号`…`)。支持去尾(如 `{Episode Title:30}`)或去头(如 `{Episode Title:-30}`)。单集标题将按需自动截取至文件系统的上限。", + "EpisodeTitleMaybePlural": "单集标题", + "EpisodeTitleRequired": "需要单集标题", "EpisodeTitleRequiredHelpText": "如果单集标题为命名格式且单集标题为「待公布」,则 在48 小时内禁用导入", + "EpisodeTitles": "单集标题", "Episodes": "剧集", + "EpisodesInSeason": "季包含 {episodeCount} 集", "EpisodesLoadError": "无法加载剧集", + "EpisodesMonitoredStatus": "单集已追踪", "Error": "错误", "ErrorLoadingContent": "加载此内容时出现错误", "ErrorLoadingContents": "加载内容出错", @@ -670,6 +704,10 @@ "Events": "事件", "Example": "示例", "Exception": "例外", + "ExcludeUnknownSeriesItems": "排除未知剧集条目", + "ExcludedReleaseProfile": "已排除的版本配置", + "ExcludedReleaseProfiles": "已排除的版本配置", + "ExcludedTags": "已排除的标签", "Existing": "已存在", "ExistingSeries": "已存在剧集", "ExistingTag": "已有标签", @@ -681,6 +719,7 @@ "ExtraFileExtensionsHelpText": "要导入的额外文件列表,以逗号分隔(.nfo 文件会被导入为 .nfo-orig)", "ExtraFileExtensionsHelpTextsExamples": "示例:’.sub,.nfo‘ 或 ’sub,nfo‘", "Failed": "失败", + "FailedAt": "失败于:{date}", "FailedToFetchSettings": "设置同步失败", "FailedToFetchUpdates": "获取更新失败", "FailedToLoadCustomFiltersFromApi": "未能从API加载自定义过滤器", @@ -703,6 +742,7 @@ "FileManagement": "文件管理", "FileNameTokens": "文件名标记", "FileNames": "文件名", + "FileSize": "文件大小", "Filename": "文件名", "Files": "文件", "Filter": "过滤", @@ -728,10 +768,12 @@ "FilterNotInNext": "不在下一个", "FilterSeriesPlaceholder": "过滤剧集", "FilterStartsWith": "以...开头", + "Filters": "过滤器", "FinaleTooltip": "剧集或季完结", "FirstDayOfWeek": "每周的第一天", "Fixed": "已修复", "Folder": "文件夹", + "FolderNameTokens": "文件夹名称标记", "Folders": "文件夹", "Forecast": "预报表", "FormatAgeDay": "天", @@ -751,6 +793,7 @@ "Formats": "格式", "Forums": "论坛", "FreeSpace": "剩余空间", + "Friday": "星期五", "From": "来自", "FullColorEvents": "全彩事件", "FullColorEventsHelpText": "改变样式,用状态颜色为整个事件着色,而不仅仅是左边缘。不适用于议程", @@ -763,14 +806,18 @@ "Global": "全局", "Grab": "抓取", "GrabId": "抓取ID", - "GrabRelease": "抓取版本", + "GrabRelease": "抓取资源", "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName}无法确定这个发布版本是哪部剧集的哪一集,{appName}可能无法自动导入此版本,你想要获取“{title}”吗?", "GrabSelected": "抓取已选", "Grabbed": "已抓取", + "GrabbedAt": "抓取于:{date}", "Group": "组", "HardlinkCopyFiles": "硬链接/复制文件", "HasMissingSeason": "有缺失的季", + "HasUnmonitoredSeason": "含有未追踪的季", "Health": "健康度", + "HealthIssue": "1 个健康度问题", + "HealthIssues": "{count} 个健康度问题", "HealthMessagesInfoBox": "您可以通过单击行尾的 wiki 链接(图书图标)或检查 [日志]({link}) 来获取导致此类运行状态消息的更多信息。如果您在理解此类信息方面有困难,您可以通过以下链接联系我们以获得支持。", "Here": "这里", "HiddenClickToShow": "隐藏,点击显示", @@ -807,6 +854,10 @@ "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", "Ignored": "已忽略", "IgnoredAddresses": "已忽略地址", + "ImageBanner": "横幅", + "ImageFanart": "同人图", + "ImagePoster": "海报", + "ImageSeason": "季", "Images": "图像", "ImdbId": "IMDb ID", "Implementation": "执行", @@ -826,11 +877,93 @@ "ImportListSearchForMissingEpisodes": "搜索缺失集", "ImportListSearchForMissingEpisodesHelpText": "将系列添加到{appName}后,自动搜索缺失的剧集", "ImportListSettings": "导入列表设置", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "可能因为部分抓取,所有列表需要手动操作", "ImportListStatusAllUnavailableHealthCheckMessage": "所有的列表因错误不可用", "ImportListStatusUnavailableHealthCheckMessage": "列表因错误不可用:{importListNames}", "ImportLists": "导入列表", + "ImportListsAniListSettingsAuthenticateWithAniList": "验证 AniList 账号", + "ImportListsAniListSettingsImportCancelled": "导入已取消剧集", + "ImportListsAniListSettingsImportCancelledHelpText": "媒体:剧集已取消", + "ImportListsAniListSettingsImportCompleted": "导入已看过", + "ImportListsAniListSettingsImportCompletedHelpText": "列表:已看过", + "ImportListsAniListSettingsImportDropped": "导入已弃剧", + "ImportListsAniListSettingsImportDroppedHelpText": "列表:已弃剧", + "ImportListsAniListSettingsImportFinished": "导入完结剧", + "ImportListsAniListSettingsImportFinishedHelpText": "媒体:所有集数完结", + "ImportListsAniListSettingsImportHiatus": "导入断更剧", + "ImportListsAniListSettingsImportHiatusHelpText": "媒体:剧集断更中", + "ImportListsAniListSettingsImportNotYetReleased": "导入未播出剧集", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "媒体:尚未播出", + "ImportListsAniListSettingsImportPaused": "导入暂停观看剧集", + "ImportListsAniListSettingsImportPausedHelpText": "列表:暂停观看", + "ImportListsAniListSettingsImportPlanning": "导入:计划观看", + "ImportListsAniListSettingsImportPlanningHelpText": "列表:计划观看", + "ImportListsAniListSettingsImportReleasing": "导入播出中剧集", + "ImportListsAniListSettingsImportReleasingHelpText": "媒体:新单集播出中", + "ImportListsAniListSettingsImportRepeating": "导入重复观看", + "ImportListsAniListSettingsImportRepeatingHelpText": "列表:重复观看中", + "ImportListsAniListSettingsImportWatching": "导入正在观看", + "ImportListsAniListSettingsImportWatchingHelpText": "列表:观看中", + "ImportListsAniListSettingsUsernameHelpText": "导入列表的用户名", + "ImportListsCustomListSettingsName": "自定义列表", + "ImportListsCustomListSettingsUrl": "列表链接", + "ImportListsCustomListSettingsUrlHelpText": "剧单的链接", + "ImportListsCustomListValidationAuthenticationFailure": "验证失败", + "ImportListsCustomListValidationConnectionError": "无法请求该链接。状态码:{exceptionStatusCode}", + "ImportListsImdbSettingsListId": "列表 ID", + "ImportListsImdbSettingsListIdHelpText": "IMDB 列表 ID", "ImportListsLoadError": "无法加载导入列表", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "从 MyAnimeList 验证", + "ImportListsMyAnimeListSettingsListStatus": "列表状态", + "ImportListsMyAnimeListSettingsListStatusHelpText": "你想要导入的列表类型,设为“全部”将导入全部列表", + "ImportListsMyAnimeListSettingsScore": "最低分数", + "ImportListsMyAnimeListSettingsScoreHelpText": "剧集的最低导入分数", + "ImportListsPlexSettingsAuthenticateWithPlex": "验证 Plex.tv 账户", + "ImportListsPlexSettingsWatchlistName": "Plex 观看清单", + "ImportListsPlexSettingsWatchlistRSSName": "Plex 观看清单 RSS", + "ImportListsSettingsAccessToken": "访问 Token", + "ImportListsSettingsAuthUser": "认证用户", + "ImportListsSettingsExpires": "过期", + "ImportListsSettingsRefreshToken": "刷新 Token", + "ImportListsSettingsRssUrl": "RSS 链接", "ImportListsSettingsSummary": "从另一个 {appName} 实例或 Trakt 列表导入并管理列表排除项", + "ImportListsSimklSettingsAuthenticatewithSimkl": "验证 Simkl 账户", + "ImportListsSimklSettingsListType": "列表类型", + "ImportListsSimklSettingsListTypeHelpText": "你想导入的列表类型", + "ImportListsSimklSettingsName": "Simkl 用户观看清单", + "ImportListsSimklSettingsShowType": "剧集类型", + "ImportListsSimklSettingsShowTypeHelpText": "你想导入的剧集类型", + "ImportListsSimklSettingsUserListTypeCompleted": "已观看", + "ImportListsSimklSettingsUserListTypeDropped": "已弃剧", + "ImportListsSimklSettingsUserListTypeHold": "暂停观看", + "ImportListsSimklSettingsUserListTypePlanToWatch": "计划观看", + "ImportListsSimklSettingsUserListTypeWatching": "观看中", + "ImportListsSonarrSettingsApiKeyHelpText": "从 {appName} 导入所需的 API 密钥", + "ImportListsSonarrSettingsFullUrl": "完整链接", + "ImportListsSonarrSettingsFullUrlHelpText": "网络地址:包含 {appName} 的端口", + "ImportListsSonarrSettingsQualityProfilesHelpText": "该导入源所的压片配置", + "ImportListsSonarrSettingsRootFoldersHelpText": "该导入源的根目录", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "同步季追踪状态", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "从 {appName} 中同步季追踪状态,若开启则“追踪“将被忽略", + "ImportListsSonarrSettingsTagsHelpText": "该导入源的标签", + "ImportListsSonarrValidationInvalidUrl": "{appName} 的网络路径不可用,你是否未提供基本 URL?", + "ImportListsTraktSettingsAdditionalParameters": "额外参数", + "ImportListsTraktSettingsAdditionalParametersHelpText": "额外的 Trakt API 参数", + "ImportListsTraktSettingsAuthenticateWithTrakt": "验证 Trakt 账户", + "ImportListsTraktSettingsGenres": "类型", + "ImportListsTraktSettingsGenresSeriesHelpText": "通过 Trakt 的 Genre Slug (逗号隔开)过滤剧集,仅对热门清单", + "ImportListsTraktSettingsLimit": "上限", + "ImportListsTraktSettingsLimitSeriesHelpText": "抓取的剧集数量上限", + "ImportListsTraktSettingsListName": "清单名称", + "ImportListsTraktSettingsListNameHelpText": "待导入的清单名称,必须为公开或你可访问的清单", + "ImportListsTraktSettingsListType": "清单类型", + "ImportListsTraktSettingsListTypeHelpText": "要导入的清单类型", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "已期待剧集", + "ImportListsTraktSettingsPopularListTypePopularShows": "热门剧集", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "史上最推荐剧集", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "月度推荐剧集", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "每周推荐剧集", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "年度推荐剧集", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "如果可能,在下载完成后自动处理", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "如果可能,启用完整的下载处理(不支持多台计算机)", "ImportMechanismHandlingDisabledHealthCheckMessage": "启用下载完成处理", @@ -1278,6 +1411,8 @@ "NotificationsPushoverSettingsRetryHelpText": "紧急警报的重试间隔,最少 30 秒", "NotificationsPushoverSettingsSound": "声音", "NotificationsPushoverSettingsSoundHelpText": "通知声音,留空使用默认声音", + "NotificationsPushoverSettingsTtl": "有效期", + "NotificationsPushoverSettingsTtlHelpText": "消息过期前的秒数。设为 0 表示永久有效(永不过期)", "NotificationsPushoverSettingsUserKey": "用户密钥", "NotificationsSendGridSettingsApiKeyHelpText": "SendGrid 生成的 API 密钥", "NotificationsSettingsUpdateLibrary": "更新资源库", @@ -1381,6 +1516,7 @@ "OrganizeSelectedSeriesModalConfirmation": "你确定要整理 {count} 个选定剧集中的所有文件?", "OrganizeSelectedSeriesModalHeader": "整理选定的剧集", "Original": "原始", + "OriginalCountry": "原国家", "OriginalLanguage": "原语言", "Other": "其他", "OutputPath": "输出路径", diff --git a/src/NzbDrone.Core/Localization/Core/zh_Hans.json b/src/NzbDrone.Core/Localization/Core/zh_Hans.json index dabfa417a..bcd21d3e2 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_Hans.json +++ b/src/NzbDrone.Core/Localization/Core/zh_Hans.json @@ -2,5 +2,6 @@ "About": "关于", "Actions": "操作", "Activity": "活动", - "AddANewPath": "Add a new path" + "AddANewPath": "Add a new path", + "AddCondition": "001" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_TW.json b/src/NzbDrone.Core/Localization/Core/zh_TW.json index f9a751450..d9d9e6ee5 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_TW.json +++ b/src/NzbDrone.Core/Localization/Core/zh_TW.json @@ -13,6 +13,7 @@ "AddConditionError": "無法加入新的條件,請重新嘗試。", "AddConditionImplementation": "新增條件 - {implementationName}", "AddConnection": "新增連接", + "AddConnectionError": "無法新增新的連線,請重試。", "AddConnectionImplementation": "新增連接 - {implementationName}", "AddCustomFilter": "新增自定義過濾器", "AddCustomFormat": "加入自訂格式", @@ -64,6 +65,9 @@ "BlocklistRelease": "封鎖清單版本", "BlocklistReleases": "封鎖清單版本", "NotificationsPushoverSettingsExpireHelpText": "緊急警報的最大重試時間,最長為 86400 秒 (24 小時)", + "NotificationsPushoverSettingsTtl": "有效期限", + "NotificationsPushoverSettingsTtlHelpText": "訊息過期前的秒數。設置為 0 表示無期限", + "NotificationsPushoverSettingsUserKey": "使用者金鑰", "UnselectAll": "取消全選", "UpdateAppDirectlyLoadError": "無法直接更新 {appName},", "UpdateAvailableHealthCheckMessage": "可用的新版本: {version}", diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs new file mode 100644 index 000000000..a3ee57b91 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/DeleteSeriesFilesCommand.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class DeleteSeriesFilesCommand : Command + { + public List SeriesIds { get; set; } + + public override bool SendUpdatesToClient => true; + public override bool RequiresDiskAccess => true; + + public DeleteSeriesFilesCommand() + { + SeriesIds = new List(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 3584851ab..be003b5e4 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -187,6 +187,7 @@ private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); var videoFiles = _diskScanService.FilterPaths(directoryInfo.FullName, _diskScanService.GetVideoFiles(directoryInfo.FullName)); + var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title); if (downloadClientItem == null) { @@ -202,7 +203,17 @@ private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode } } - var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, downloadClientItem, folderInfo, true); + if (downloadClientItemInfo is { IsMultiSeason: true }) + { + _logger.Debug("Download client item is marked as multi-season, not processing automatically to avoid importing incorrect files"); + + return new List + { + RejectionResult(ImportRejectionReason.MultiSeason, "Multi-season download, unable to import automatically") + }; + } + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, downloadClientItem, downloadClientItemInfo, folderInfo, true); var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); if (importMode == ImportMode.Auto) @@ -328,7 +339,8 @@ private List ProcessFile(FileInfo fileInfo, ImportMode importMode, } } - var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, downloadClientItem, null, true); + var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title); + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, series, downloadClientItem, downloadClientItemInfo, null, true); return _importApprovedEpisodes.Import(decisions, true, downloadClientItem, importMode); } 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 e03748e98..c07ed59e4 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 @@ -30,11 +30,13 @@ public AugmentQualityResult AugmentQuality(LocalEpisode localEpisode, DownloadCl var height = localEpisode.MediaInfo.Height; var source = QualitySource.Unknown; var sourceConfidence = Confidence.Default; - var title = localEpisode.MediaInfo.Title; + var title = localEpisode.MediaInfo.Title?.Trim(); if (title.IsNotNullOrWhiteSpace()) { - var parsedQuality = QualityParser.ParseQualityName(title.Trim()); + _logger.Debug("Parsing quality from media info title '{0}'", title); + + var parsedQuality = QualityParser.ParseQualityName(title); // Only use the quality if it's not unknown and the source is from the name (which is MediaInfo's title in this case) if (parsedQuality.Quality.Source != QualitySource.Unknown && diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 90f15a348..0db1234e3 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using NLog; using NzbDrone.Common.Disk; @@ -85,24 +84,8 @@ public List Import(List decisions, bool newDownloa continue; } - var episodeFile = new EpisodeFile(); - episodeFile.DateAdded = DateTime.UtcNow; - episodeFile.SeriesId = localEpisode.Series.Id; - episodeFile.Path = localEpisode.Path.CleanFilePath(); + var episodeFile = localEpisode.ToEpisodeFile(); 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; - 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. - episodeFile.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? - localEpisode.FolderEpisodeInfo?.ReleaseType ?? - localEpisode.FileEpisodeInfo.ReleaseType; if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true) { @@ -118,23 +101,12 @@ public List Import(List decisions, bool newDownloa // Prefer the release type from the grabbed history if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("releaseType"), true, out ReleaseType releaseType)) { - episodeFile.ReleaseType = releaseType; + if (releaseType != ReleaseType.Unknown) + { + episodeFile.ReleaseType = releaseType; + } } } - else - { - episodeFile.IndexerFlags = localEpisode.IndexerFlags; - episodeFile.ReleaseType = localEpisode.ReleaseType; - } - - // 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) @@ -153,15 +125,20 @@ public List Import(List decisions, bool newDownloa if (newDownload) { - episodeFile.SceneName = localEpisode.SceneName; - episodeFile.OriginalFilePath = GetOriginalFilePath(downloadClientItem, localEpisode); + if (downloadClientItem is { OutputPath.IsEmpty: false }) + { + var outputDirectory = downloadClientItem.OutputPath.Directory.ToString(); + + if (outputDirectory.IsParentPath(localEpisode.Path)) + { + episodeFile.OriginalFilePath = outputDirectory.GetRelativePath(localEpisode.Path); + } + } oldFiles = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly).OldFiles; } else { - episodeFile.RelativePath = localEpisode.Series.Path.GetRelativePath(episodeFile.Path); - // Delete existing files from the DB mapped to this path var previousFiles = _mediaFileService.GetFilesWithRelativePath(localEpisode.Series.Id, episodeFile.RelativePath); @@ -228,42 +205,5 @@ public List Import(List decisions, bool newDownloa return importResults; } - - private string GetOriginalFilePath(DownloadClientItem downloadClientItem, LocalEpisode localEpisode) - { - var path = localEpisode.Path; - - if (downloadClientItem != null && !downloadClientItem.OutputPath.IsEmpty) - { - var outputDirectory = downloadClientItem.OutputPath.Directory.ToString(); - - if (outputDirectory.IsParentPath(path)) - { - return outputDirectory.GetRelativePath(path); - } - } - - var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; - - if (folderEpisodeInfo != null) - { - var folderPath = path.GetAncestorPath(folderEpisodeInfo.ReleaseTitle); - - if (folderPath != null) - { - return folderPath.GetParentPath().GetRelativePath(path); - } - } - - var parentPath = path.GetParentPath(); - var grandparentPath = parentPath.GetParentPath(); - - if (grandparentPath != null) - { - return grandparentPath.GetRelativePath(path); - } - - return Path.GetFileName(path); - } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 762a5a4f3..18288cd96 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -4,7 +4,6 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; -using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; @@ -17,8 +16,8 @@ public interface IMakeImportDecision { List GetImportDecisions(List videoFiles, Series series); List GetImportDecisions(List videoFiles, Series series, bool filterExistingFiles); - List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource); - List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles); + List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource); + List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles); ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); } @@ -30,7 +29,7 @@ public class ImportDecisionMaker : IMakeImportDecision private readonly IDiskProvider _diskProvider; private readonly IDetectSample _detectSample; private readonly ITrackedDownloadService _trackedDownloadService; - private readonly ICustomFormatCalculationService _formatCalculator; + private readonly ILocalEpisodeCustomFormatCalculationService _formatCalculator; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, @@ -39,7 +38,7 @@ public ImportDecisionMaker(IEnumerable speci IDiskProvider diskProvider, IDetectSample detectSample, ITrackedDownloadService trackedDownloadService, - ICustomFormatCalculationService formatCalculator, + ILocalEpisodeCustomFormatCalculationService formatCalculator, Logger logger) { _specifications = specifications; @@ -59,27 +58,20 @@ public List GetImportDecisions(List videoFiles, Series s public List GetImportDecisions(List videoFiles, Series series, bool filterExistingFiles) { - return GetImportDecisions(videoFiles, series, null, null, false, filterExistingFiles); + return GetImportDecisions(videoFiles, series, null, null, null, false, filterExistingFiles); } - public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource) + public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource) { - return GetImportDecisions(videoFiles, series, downloadClientItem, folderInfo, sceneSource, true); + return GetImportDecisions(videoFiles, series, downloadClientItem, downloadClientItemInfo, folderInfo, sceneSource, true); } - public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles) + public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles) { var newFiles = filterExistingFiles ? _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series) : videoFiles.ToList(); _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count); - ParsedEpisodeInfo downloadClientItemInfo = null; - - if (downloadClientItem != null) - { - downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title); - } - // If not importing from a scene source (series folder for example), then assume all files are not samples // to avoid using media info on every file needlessly (especially if Analyse Media Files is disabled). var nonSampleVideoFileCount = sceneSource ? GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo) : videoFiles.Count; @@ -158,8 +150,7 @@ private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem } } - localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); - localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + _formatCalculator.UpdateEpisodeCustomFormats(localEpisode); decision = GetDecision(localEpisode, downloadClientItem); } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs index 7a0ce4170..3a8f49d4c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportRejectionReason.cs @@ -36,5 +36,7 @@ public enum ImportRejectionReason UnverifiedSceneMapping, NotQualityUpgrade, NotRevisionUpgrade, - NotCustomFormatUpgrade + NotCustomFormatUpgrade, + NotCustomFormatUpgradeAfterRename, + MultiSeason } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationService.cs new file mode 100644 index 000000000..5ddf0e957 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/LocalEpisodeCustomFormatCalculationService.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport; + +public interface ILocalEpisodeCustomFormatCalculationService +{ + public List ParseEpisodeCustomFormats(LocalEpisode localEpisode); + public void UpdateEpisodeCustomFormats(LocalEpisode localEpisode); +} + +public class LocalEpisodeCustomFormatCalculationService : ILocalEpisodeCustomFormatCalculationService +{ + private readonly IBuildFileNames _fileNameBuilder; + private readonly ICustomFormatCalculationService _formatCalculator; + + public LocalEpisodeCustomFormatCalculationService(IBuildFileNames fileNameBuilder, ICustomFormatCalculationService formatCalculator) + { + _fileNameBuilder = fileNameBuilder; + _formatCalculator = formatCalculator; + } + + public List ParseEpisodeCustomFormats(LocalEpisode localEpisode) + { + var fileNameUsedForCustomFormatCalculation = _fileNameBuilder.BuildFileName(localEpisode.Episodes, localEpisode.Series, localEpisode.ToEpisodeFile()); + return _formatCalculator.ParseCustomFormat(localEpisode, fileNameUsedForCustomFormatCalculation); + } + + public void UpdateEpisodeCustomFormats(LocalEpisode localEpisode) + { + var fileNameUsedForCustomFormatCalculation = _fileNameBuilder.BuildFileName(localEpisode.Episodes, localEpisode.Series, localEpisode.ToEpisodeFile()); + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode, fileNameUsedForCustomFormatCalculation); + localEpisode.FileNameUsedForCustomFormatCalculation = fileNameUsedForCustomFormatCalculation; + localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + + localEpisode.OriginalFileNameCustomFormats = _formatCalculator.ParseCustomFormat(localEpisode, Path.GetFileName(localEpisode.Path)); + localEpisode.OriginalFileNameCustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.OriginalFileNameCustomFormats) ?? 0; + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 5adf30d8e..9b80a82ff 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -41,6 +41,7 @@ public class ManualImportService : IExecute, IManualImportS private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; private readonly IMediaFileService _mediaFileService; private readonly ICustomFormatCalculationService _formatCalculator; + private readonly ILocalEpisodeCustomFormatCalculationService _localEpisodeFormatCalculator; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -55,6 +56,7 @@ public ManualImportService(IDiskProvider diskProvider, ITrackedDownloadService trackedDownloadService, IDownloadedEpisodesImportService downloadedEpisodesImportService, IMediaFileService mediaFileService, + ILocalEpisodeCustomFormatCalculationService localEpisodeFormatCalculator, ICustomFormatCalculationService formatCalculator, IEventAggregator eventAggregator, Logger logger) @@ -70,6 +72,7 @@ public ManualImportService(IDiskProvider diskProvider, _trackedDownloadService = trackedDownloadService; _downloadedEpisodesImportService = downloadedEpisodesImportService; _mediaFileService = mediaFileService; + _localEpisodeFormatCalculator = localEpisodeFormatCalculator; _formatCalculator = formatCalculator; _eventAggregator = eventAggregator; _logger = logger; @@ -180,8 +183,7 @@ public ManualImportItem ReprocessItem(string path, string downloadId, int series localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; localEpisode.ReleaseType = releaseType; - localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); - localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + _localEpisodeFormatCalculator.UpdateEpisodeCustomFormats(localEpisode); // Augment episode file so imported files have all additional information an automatic import would localEpisode = _aggregationService.Augment(localEpisode, downloadClientItem); @@ -288,9 +290,10 @@ private List ProcessFolder(string rootFolder, string baseFolde return processedFiles.Concat(processedFolders).Where(i => i != null).ToList(); } + var downloadClientItemInfo = downloadClientItem == null ? null : Parser.Parser.ParseTitle(downloadClientItem.Title); var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); var seriesFiles = _diskScanService.FilterPaths(rootFolder, _diskScanService.GetVideoFiles(baseFolder).ToList()); - var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, folderInfo, SceneSource(series, baseFolder), filterExistingFiles); + var decisions = _importDecisionMaker.GetImportDecisions(seriesFiles, series, downloadClientItem, downloadClientItemInfo, folderInfo, SceneSource(series, baseFolder), filterExistingFiles); return decisions.Select(decision => MapItem(decision, rootFolder, downloadId, directoryInfo.Name)).ToList(); } @@ -343,9 +346,12 @@ private ManualImportItem ProcessFile(string rootFolder, string baseFolder, strin null); } + var downloadClientItemInfo = trackedDownload?.DownloadItem == null ? null : Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); + var importDecisions = _importDecisionMaker.GetImportDecisions(new List { file }, series, trackedDownload?.DownloadItem, + downloadClientItemInfo, null, SceneSource(series, baseFolder)); @@ -445,8 +451,7 @@ private ManualImportItem MapItem(ImportDecision decision, string rootFolder, str if (decision.LocalEpisode.Series != null) { item.Series = decision.LocalEpisode.Series; - - item.CustomFormats = _formatCalculator.ParseCustomFormat(decision.LocalEpisode); + item.CustomFormats = _localEpisodeFormatCalculator.ParseEpisodeCustomFormats(decision.LocalEpisode); item.CustomFormatScore = item.Series.QualityProfile?.Value.CalculateCustomFormatScore(item.CustomFormats) ?? 0; } @@ -537,8 +542,7 @@ public void Execute(ManualImportCommand message) localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags; localEpisode.ReleaseType = file.ReleaseType; - localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); - localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; + _localEpisodeFormatCalculator.UpdateEpisodeCustomFormats(localEpisode); // TODO: Cleanup non-tracked downloads diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index ae824ffb9..585213ccd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -62,6 +62,8 @@ public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClien var currentFormatScore = qualityProfile.CalculateCustomFormatScore(currentFormats); var newFormats = localEpisode.CustomFormats; var newFormatScore = localEpisode.CustomFormatScore; + var newFormatsBeforeRename = localEpisode.OriginalFileNameCustomFormats; + var newFormatScoreBeforeRename = localEpisode.OriginalFileNameCustomFormatScore; if (qualityCompare == 0 && newFormatScore < currentFormatScore) { @@ -71,6 +73,18 @@ public ImportSpecDecision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClien currentFormats != null ? currentFormats.ConcatToString() : "", currentFormatScore); + if (newFormatScoreBeforeRename > currentFormatScore) + { + return ImportSpecDecision.Reject(ImportRejectionReason.NotCustomFormatUpgradeAfterRename, + "Not a Custom Format upgrade for existing episode file(s). AfterRename: [{0}] ({1}) do not improve on Existing: [{2}] ({3}) even though BeforeRename: [{4}] ({5}) did.", + newFormats != null ? newFormats.ConcatToString() : "", + newFormatScore, + currentFormats != null ? currentFormats.ConcatToString() : "", + currentFormatScore, + newFormatsBeforeRename != null ? newFormatsBeforeRename.ConcatToString() : "", + newFormatScoreBeforeRename); + } + 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() : "", diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index bd8d66025..463346976 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -4,11 +4,15 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; @@ -20,6 +24,7 @@ public interface IDeleteMediaFiles } public class MediaFileDeletionService : IDeleteMediaFiles, + IExecute, IHandleAsync, IHandle { @@ -27,7 +32,9 @@ public class MediaFileDeletionService : IDeleteMediaFiles, private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; private readonly ISeriesService _seriesService; + private readonly IRootFolderService _rootFolderService; private readonly IConfigService _configService; + private readonly ICommandResultReporter _commandResultReporter; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -35,7 +42,9 @@ public MediaFileDeletionService(IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, ISeriesService seriesService, + IRootFolderService rootFolderService, IConfigService configService, + ICommandResultReporter commandResultReporter, IEventAggregator eventAggregator, Logger logger) { @@ -43,7 +52,9 @@ public MediaFileDeletionService(IDiskProvider diskProvider, _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; _seriesService = seriesService; + _rootFolderService = rootFolderService; _configService = configService; + _commandResultReporter = commandResultReporter; _eventAggregator = eventAggregator; _logger = logger; } @@ -51,7 +62,7 @@ public MediaFileDeletionService(IDiskProvider diskProvider, public void DeleteEpisodeFile(Series series, EpisodeFile episodeFile) { var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); - var rootFolder = _diskProvider.GetParentFolder(series.Path); + var rootFolder = _rootFolderService.GetBestRootFolderPath(series.Path); if (!_diskProvider.FolderExists(rootFolder)) { @@ -88,6 +99,81 @@ public void DeleteEpisodeFile(Series series, EpisodeFile episodeFile) _eventAggregator.PublishEvent(new DeleteCompletedEvent()); } + public void Execute(DeleteSeriesFilesCommand message) + { + foreach (var seriesId in message.SeriesIds) + { + try + { + var series = _seriesService.GetSeries(seriesId); + var mediaFiles = _mediaFileService.GetFilesBySeries(seriesId); + + _logger.ProgressDebug("{0}: Deleting episode files}", series.Title); + + if (mediaFiles.Count == 0) + { + _logger.Debug("No files found for series: {0}", series.Title); + continue; + } + + var rootFolder = _rootFolderService.GetBestRootFolderPath(series.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + if (_diskProvider.GetDirectories(rootFolder).Empty()) + { + _logger.Warn("Series' root folder ({0}) is empty.", rootFolder); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Warn("Series' folder ({0}) does not exist.", series.Path); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + foreach (var episodeFile in mediaFiles) + { + var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + + if (_diskProvider.FileExists(fullPath)) + { + _logger.Info("Deleting episode file: {0}", fullPath); + + var subfolder = _diskProvider.GetParentFolder(series.Path).GetRelativePath(_diskProvider.GetParentFolder(fullPath)); + + try + { + _recycleBinProvider.DeleteFile(fullPath, subfolder); + } + catch (Exception e) + { + _logger.Error(e, "Unable to delete episode file"); + _commandResultReporter.Report(CommandResult.Indeterminate); + continue; + } + + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + } + } + + _logger.ProgressDebug("{0}: Deleted episode files", series.Title); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to delete files for series with ID: {0}", seriesId); + _commandResultReporter.Report(CommandResult.Indeterminate); + } + } + } + public void HandleAsync(SeriesDeletedEvent message) { if (message.DeleteFiles) diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 66dde9cc1..277633ec5 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -31,7 +31,7 @@ public static decimal FormatAudioChannels(MediaInfoAudioStreamModel audioStream) public static string FormatAudioCodec(MediaInfoAudioStreamModel audioStream, string sceneName) { - if (audioStream.Format == null) + if (audioStream?.Format == null) { return null; } @@ -155,7 +155,7 @@ public static string FormatAudioCodec(MediaInfoAudioStreamModel audioStream, str public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName) { - if (mediaInfo.VideoFormat == null) + if (mediaInfo?.VideoFormat == null) { return null; } @@ -270,7 +270,7 @@ public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoAudioStreamModel audioStream) { - if (audioStream.ChannelPositions == null) + if (audioStream?.ChannelPositions == null) { return 0; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index cde1db229..bf2a6bc1d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -85,7 +85,12 @@ public bool UpdateMediaInfo(EpisodeFile episodeFile, Series series) } episodeFile.MediaInfo = updatedMediaInfo; - _mediaFileService.Update(episodeFile); + + if (episodeFile.Id != 0) + { + _mediaFileService.Update(episodeFile); + } + _logger.Debug("Updated MediaInfo for '{0}'", path); return true; diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 073b510ce..32742bc96 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -85,13 +85,12 @@ public MediaInfoModel GetMediaInfo(string filename) mediaInfoModel.RawStreamData = string.Concat(analysis.OutputData); mediaInfoModel.AudioStreams = analysis.AudioStreams? - .Where(stream => stream.Language.IsNotNullOrWhiteSpace()) .OrderBy(stream => stream.Index) .Select(stream => { var model = new MediaInfoAudioStreamModel { - Language = stream.Language, + Language = stream.Language.IsNotNullOrWhiteSpace() ? stream.Language : "und", Format = stream.CodecName, CodecId = stream.CodecTagString, Profile = stream.Profile, diff --git a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs index 89d9c2340..b7e6e7907 100644 --- a/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs +++ b/src/NzbDrone.Core/MediaFiles/RecycleBinProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using NLog; using NzbDrone.Common.Disk; @@ -176,20 +177,26 @@ public void Cleanup() _logger.Info("Removing items older than {0} days from the recycling bin", cleanupDays); + var removedFiles = new List(); + var skippedFiles = new List(); + foreach (var file in _diskProvider.GetFiles(_configService.RecycleBin, true)) { if (_diskProvider.FileGetLastWrite(file).AddDays(cleanupDays) > DateTime.UtcNow) { _logger.Debug("File hasn't expired yet, skipping: {0}", file); + skippedFiles.Add(file); continue; } + removedFiles.Add(file); + _logger.Debug("File expired, deleting: {0}", file); _diskProvider.DeleteFile(file); } _diskProvider.RemoveEmptySubfolders(_configService.RecycleBin); - _logger.Debug("Recycling Bin has been cleaned up."); + _logger.Debug("Recycling Bin has been cleaned up. Removed: {0}. Skipped: {1}", removedFiles.Count, skippedFiles.Count); } private void SetLastWriteTime(string file, DateTime dateTime) @@ -197,13 +204,16 @@ private void SetLastWriteTime(string file, DateTime dateTime) // Swallow any IOException that may be thrown due to "Invalid parameter" try { + _logger.Trace("Setting last write time for file: {0}", file); _diskProvider.FileSetLastWriteTime(file, dateTime); } - catch (IOException) + catch (IOException ex) { + _logger.Warn(ex, "Failed to set last write time for file: {0}", file); } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException ex) { + _logger.Warn(ex, "Failed to set last write time for file: {0}", file); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs index b7a9d7a42..6994f8559 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ShowResource.cs @@ -33,6 +33,7 @@ public ShowResource() public string Network { get; set; } public string ImdbId { get; set; } public string OriginalLanguage { get; set; } + public string OriginalCountry { get; set; } public List Actors { get; set; } public List Genres { get; set; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 883a84c45..40b59ac78 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -239,6 +239,7 @@ private Series MapSeries(ShowResource show) series.Status = MapSeriesStatus(show.Status); series.Ratings = MapRatings(show.Rating); series.Genres = show.Genres; + series.OriginalCountry = show.OriginalCountry; if (show.ContentRating.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs index 3bfa6037e..f949c25a0 100644 --- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs +++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs @@ -61,21 +61,9 @@ public override void OnGrab(GrabMessage message) var releaseGroup = remoteEpisode.ParsedEpisodeInfo.ReleaseGroup; var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "Grab"); - 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_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))); + AddInstanceVariables(environmentVariables, "Grab"); + AddSeriesVariables(environmentVariables, 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))); @@ -109,23 +97,10 @@ public override void OnDownload(DownloadMessage message) 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); + AddInstanceVariables(environmentVariables, "Download"); + AddSeriesVariables(environmentVariables, series); + environmentVariables.Add("Sonarr_IsUpgrade", message.OldFiles.Any().ToString()); - 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_Id", episodeFile.Id.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_RelativePath", episodeFile.RelativePath); @@ -182,22 +157,9 @@ public override void OnImportComplete(ImportCompleteMessage message) 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))); + AddInstanceVariables(environmentVariables, "Download"); + AddSeriesVariables(environmentVariables, 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))); @@ -238,22 +200,9 @@ public override void OnRename(Series series, List renamedFil { var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "Rename"); - 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))); + AddInstanceVariables(environmentVariables, "Rename"); + AddSeriesVariables(environmentVariables, 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 => Path.Combine(series.Path, e.EpisodeFile.RelativePath)))); @@ -270,23 +219,9 @@ public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "EpisodeFileDelete"); - environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); - environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); - environmentVariables.Add("Sonarr_EpisodeFile_DeleteReason", deleteMessage.Reason.ToString()); - 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))); + AddInstanceVariables(environmentVariables, "EpisodeFileDelete"); + AddSeriesVariables(environmentVariables, 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); @@ -311,22 +246,8 @@ public override void OnSeriesAdd(SeriesAddMessage message) var series = message.Series; var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "SeriesAdd"); - 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))); + AddInstanceVariables(environmentVariables, "SeriesAdd"); + AddSeriesVariables(environmentVariables, series); ExecuteScript(environmentVariables); } @@ -336,23 +257,8 @@ public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) var series = deleteMessage.Series; var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "SeriesDelete"); - 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_Series_DeletedFiles", deleteMessage.DeletedFiles.ToString()); + AddInstanceVariables(environmentVariables, "SeriesDelete"); + AddSeriesVariables(environmentVariables, series); ExecuteScript(environmentVariables); } @@ -361,9 +267,8 @@ public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "HealthIssue"); - environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); - environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); + AddInstanceVariables(environmentVariables, "HealthIssue"); + environmentVariables.Add("Sonarr_Health_Issue_Level", Enum.GetName(typeof(HealthCheckResult), healthCheck.Type)); environmentVariables.Add("Sonarr_Health_Issue_Message", healthCheck.Message); environmentVariables.Add("Sonarr_Health_Issue_Type", healthCheck.Source.Name); @@ -376,9 +281,8 @@ public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "HealthRestored"); - environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); - environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); + AddInstanceVariables(environmentVariables, "HealthRestored"); + environmentVariables.Add("Sonarr_Health_Restored_Level", Enum.GetName(typeof(HealthCheckResult), previousCheck.Type)); environmentVariables.Add("Sonarr_Health_Restored_Message", previousCheck.Message); environmentVariables.Add("Sonarr_Health_Restored_Type", previousCheck.Source.Name); @@ -391,9 +295,8 @@ public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { var environmentVariables = new StringDictionary(); - environmentVariables.Add("Sonarr_EventType", "ApplicationUpdate"); - environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); - environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); + AddInstanceVariables(environmentVariables, "ApplicationUpdate"); + environmentVariables.Add("Sonarr_Update_Message", updateMessage.Message); environmentVariables.Add("Sonarr_Update_NewVersion", updateMessage.NewVersion.ToString()); environmentVariables.Add("Sonarr_Update_PreviousVersion", updateMessage.PreviousVersion.ToString()); @@ -406,22 +309,9 @@ public override void OnManualInteractionRequired(ManualInteractionRequiredMessag var series = message.Series; var environmentVariables = new StringDictionary(); - 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 ?? new List())); - environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series))); + AddInstanceVariables(environmentVariables, "ManualInteractionRequired"); + AddSeriesVariables(environmentVariables, 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); @@ -496,5 +386,30 @@ private List GetTagLabels(Series series) .OrderBy(l => l) .ToList(); } + + private void AddInstanceVariables(StringDictionary environmentVariables, string eventType) + { + environmentVariables.Add("Sonarr_EventType", eventType); + environmentVariables.Add("Sonarr_InstanceName", _configFileProvider.InstanceName); + environmentVariables.Add("Sonarr_ApplicationUrl", _configService.ApplicationUrl); + } + + private void AddSeriesVariables(StringDictionary environmentVariables, Series series) + { + 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_OriginalCountry", series.OriginalCountry); + 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))); + } } } diff --git a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs index ceff60865..4d2a9f226 100644 --- a/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/Server/PlexServerService.cs @@ -141,8 +141,9 @@ private void UpdateSectionPath(string seriesRelativePath, PlexSection section, P var separator = location.Path.Contains('\\') ? "\\" : "/"; var locationRelativePath = seriesRelativePath.Replace("\\", separator).Replace("/", separator); - // Plex location paths trim trailing extraneous separator characters, so it doesn't need to be trimmed - var pathToUpdate = $"{location.Path}{separator}{locationRelativePath}"; + // Plex location paths trim trailing extraneous separator characters, + // unless it's a Windows drive letter (S:\) that needs to be trimmed. + var pathToUpdate = $"{location.Path.TrimEnd(separator)}{separator}{locationRelativePath}"; _logger.Debug("Updating section location, {0}", location.Path); _plexServerProxy.Update(section.Id, pathToUpdate, settings); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs index 6ad1ae14f..a0ca26e7e 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSeries.cs @@ -23,6 +23,7 @@ public class WebhookSeries public List Images { get; set; } public List Tags { get; set; } public Language OriginalLanguage { get; set; } + public string OriginalCountry { get; set; } public WebhookSeries() { @@ -46,6 +47,7 @@ public WebhookSeries(Series series, List tags) Images = series.Images.Select(i => new WebhookImage(i)).ToList(); Tags = tags; OriginalLanguage = series.OriginalLanguage; + OriginalCountry = series.OriginalCountry; } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 754c24a91..8f3908022 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Diacritical; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 0129c5d0c..f0e2ad0a3 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; @@ -13,13 +14,6 @@ namespace NzbDrone.Core.Parser.Model { public class LocalEpisode { - public LocalEpisode() - { - Episodes = new List(); - Languages = new List(); - CustomFormats = new List(); - } - public string Path { get; set; } public long Size { get; set; } public ParsedEpisodeInfo FileEpisodeInfo { get; set; } @@ -27,10 +21,10 @@ public LocalEpisode() public DownloadClientItem DownloadItem { get; set; } public ParsedEpisodeInfo FolderEpisodeInfo { get; set; } public Series Series { get; set; } - public List Episodes { get; set; } + public List Episodes { get; set; } = new(); public List OldFiles { get; set; } public QualityModel Quality { get; set; } - public List Languages { get; set; } + public List Languages { get; set; } = new(); public IndexerFlags IndexerFlags { get; set; } public ReleaseType ReleaseType { get; set; } public MediaInfoModel MediaInfo { get; set; } @@ -40,11 +34,14 @@ public LocalEpisode() public string ReleaseHash { get; set; } public string SceneName { get; set; } public bool OtherVideoFiles { get; set; } - public List CustomFormats { get; set; } + public List CustomFormats { get; set; } = new(); public int CustomFormatScore { get; set; } + public List OriginalFileNameCustomFormats { get; set; } = new(); + public int OriginalFileNameCustomFormatScore { get; set; } public GrabbedReleaseInfo Release { get; set; } public bool ScriptImported { get; set; } public string FileNameBeforeRename { get; set; } + public string FileNameUsedForCustomFormatCalculation { get; set; } public bool ShouldImportExtras { get; set; } public List PossibleExtraFiles { get; set; } public SubtitleTitleInfo SubtitleInfo { get; set; } @@ -75,5 +72,80 @@ public override string ToString() { return Path; } + + public string GetSceneOrFileName() + { + if (SceneName.IsNotNullOrWhiteSpace()) + { + return SceneName; + } + + if (Path.IsNotNullOrWhiteSpace()) + { + return System.IO.Path.GetFileNameWithoutExtension(Path); + } + + return string.Empty; + } + + public EpisodeFile ToEpisodeFile() + { + var episodeFile = new EpisodeFile + { + DateAdded = DateTime.UtcNow, + SeriesId = Series.Id, + Path = Path.CleanFilePath(), + Quality = Quality, + MediaInfo = MediaInfo, + Series = Series, + SeasonNumber = SeasonNumber, + Episodes = Episodes, + ReleaseGroup = ReleaseGroup, + ReleaseHash = ReleaseHash, + Languages = Languages, + IndexerFlags = IndexerFlags, + ReleaseType = ReleaseType, + SceneName = SceneName, + OriginalFilePath = GetOriginalFilePath() + }; + + if (Series.Path.IsParentPath(episodeFile.Path)) + { + episodeFile.RelativePath = Series.Path.GetRelativePath(Path.CleanFilePath()); + } + + if (episodeFile.ReleaseType == ReleaseType.Unknown) + { + episodeFile.ReleaseType = DownloadClientEpisodeInfo?.ReleaseType ?? + FolderEpisodeInfo?.ReleaseType ?? + FileEpisodeInfo?.ReleaseType ?? + ReleaseType.Unknown; + } + + return episodeFile; + } + + private string GetOriginalFilePath() + { + if (FolderEpisodeInfo != null) + { + var folderPath = Path.GetAncestorPath(FolderEpisodeInfo.ReleaseTitle); + + if (folderPath != null) + { + return folderPath.GetParentPath().GetRelativePath(Path); + } + } + + var parentPath = Path.GetParentPath(); + var grandparentPath = parentPath.GetParentPath(); + + if (grandparentPath != null) + { + return grandparentPath.GetRelativePath(Path); + } + + return System.IO.Path.GetFileName(Path); + } } } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 79be10f59..9b649a2a1 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -329,6 +329,10 @@ public static class Parser new Regex(@"^(?.+?)(?:(?:[-_. ]+?Temporada.+?|\[.+?\])\[Cap)(?:[-_. ]+(?<season>(?<!\d+)\d{1,2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])))+(?:\])", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Episodes with airdate (2018.04.28) + new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-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), @@ -343,10 +347,6 @@ public static class Parser 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) - new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Turkish tracker releases (01 BLM, 3. Blm, 04.Bolum, etc) new Regex(@"^(?<title>.+?)[_. ](?<absoluteepisode>\d{1,4})(?:[_. ]+)(?:BLM|B[oö]l[uü]m)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -447,7 +447,7 @@ public static class Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime OVA special - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<special>special|ova|ovd)).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<special>special|ova|ovd|ncop|nced)).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled) }; @@ -827,7 +827,7 @@ public static string CleanSeriesTitle(this string title) // Replace `%` with `percent` to deal with the 3% case title = PercentRegex.Replace(title, "percent"); - return NormalizeRegex.Replace(title).ToLowerInvariant().RemoveAccent(); + return NormalizeRegex.Replace(title).ToLowerInvariant().RemoveDiacritics(); } public static string NormalizeEpisodeTitle(string title) diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 30f5943e2..710328df1 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -74,13 +74,31 @@ public Series GetSeries(string title) private Series GetSeriesByAllTitles(ParsedEpisodeInfo parsedEpisodeInfo) { + var year = parsedEpisodeInfo.SeriesTitleInfo.Year; Series foundSeries = null; int? foundTvdbId = null; // Match each title individually, they must all resolve to the same tvdbid foreach (var title in parsedEpisodeInfo.SeriesTitleInfo.AllTitles) { - var series = _seriesService.FindByTitle(title); + Series series = null; + + if (year > 0) + { + series = _seriesService.FindByTitle(title, year); + + // Fall back to title + year being part of the title, this will allow + // matching series with the same name that include the year in the title. + if (series == null) + { + series = _seriesService.FindByTitle($"{title} {year}"); + } + } + else + { + series = _seriesService.FindByTitle(title); + } + var tvdbId = series?.TvdbId; if (series == null) @@ -116,6 +134,25 @@ private Series GetSeriesByAllTitles(ParsedEpisodeInfo parsedEpisodeInfo) return foundSeries; } + private Series GetSeriesAliasTitleAndYear(ParsedEpisodeInfo parsedEpisodeInfo) + { + var year = parsedEpisodeInfo.SeriesTitleInfo.Year; + var titleWithoutyear = parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear; + var tvdbId = _sceneMappingService.FindTvdbId(titleWithoutyear, parsedEpisodeInfo.ReleaseTitle, parsedEpisodeInfo.SeasonNumber); + + if (tvdbId.HasValue) + { + var series = _seriesService.FindByTvdbId(tvdbId.Value); + + if (series != null && series.Year == year) + { + return series; + } + } + + return null; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null) { return Map(parsedEpisodeInfo, tvdbId, tvRageId, imdbId, null, searchCriteria); @@ -449,6 +486,12 @@ private FindSeriesResult FindSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvd { series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, parsedEpisodeInfo.SeriesTitleInfo.Year); matchType = SeriesMatchType.Title; + + if (series == null) + { + series = GetSeriesAliasTitleAndYear(parsedEpisodeInfo); + matchType = SeriesMatchType.Alias; + } } if (series == null && tvdbId > 0) diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs index c740dd48b..ee4b77447 100644 --- a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs @@ -9,6 +9,8 @@ public class ReleaseProfile : ModelBase public bool Enabled { get; set; } public List<string> Required { get; set; } public List<string> Ignored { get; set; } + public bool AirDateRestriction { get; set; } + public int AirDateGracePeriod { get; set; } public List<int> IndexerIds { get; set; } public HashSet<int> Tags { get; set; } public HashSet<int> ExcludedTags { get; set; } diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs index cf615a731..5fdf01519 100644 --- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs @@ -4,6 +4,8 @@ using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.SeriesStats { @@ -21,6 +23,8 @@ public class SeasonStatistics : ResultSet public int MonitoredEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public string ReleaseGroupsString { get; set; } + public string ReleaseTypesString { get; set; } + public string EpisodeFileQualitiesString { get; set; } public DateTime? NextAiring { @@ -110,5 +114,42 @@ public List<string> ReleaseGroups return releasegroups; } } + + public List<ReleaseType> ReleaseTypes + { + get + { + if (ReleaseTypesString.IsNullOrWhiteSpace()) + { + return []; + } + + return ReleaseTypesString + .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(int.Parse) + .Distinct() + .Where(type => Enum.IsDefined(typeof(ReleaseType), type)) + .Select(type => (ReleaseType)type) + .ToList(); + } + } + + public List<Quality> EpisodeFileQualities + { + get + { + if (EpisodeFileQualitiesString.IsNullOrWhiteSpace()) + { + return new List<Quality>(); + } + + return EpisodeFileQualitiesString + .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(int.Parse) + .Distinct() + .Select(Quality.FindById) + .ToList(); + } + } } } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs index e3f10d24f..1d4d57902 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.SeriesStats { @@ -16,6 +18,8 @@ public class SeriesStatistics : ResultSet public int MonitoredEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public List<string> ReleaseGroups { get; set; } + public List<ReleaseType> ReleaseTypes { get; set; } + public List<Quality> EpisodeFileQualities { get; set; } public List<SeasonStatistics> SeasonStatistics { get; set; } } } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs index 68b4f91ff..2af4e57ce 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs @@ -49,6 +49,8 @@ private List<SeasonStatistics> MapResults(List<SeasonStatistics> episodesResult, e.SizeOnDisk = file?.SizeOnDisk ?? 0; e.ReleaseGroupsString = file?.ReleaseGroupsString; + e.ReleaseTypesString = file?.ReleaseTypesString; + e.EpisodeFileQualitiesString = file?.EpisodeFileQualitiesString; }); return episodesResult; @@ -96,7 +98,9 @@ private SqlBuilder EpisodeFilesBuilder() .Select(@"""SeriesId"", ""SeasonNumber"", SUM(COALESCE(""Size"", 0)) AS SizeOnDisk, - GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString") + GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString, + GROUP_CONCAT(""ReleaseType"", '|') AS ReleaseTypesString, + GROUP_CONCAT(JSON_EXTRACT(""Quality"", '$.quality'), '|') AS EpisodeFileQualitiesString") .GroupBy<EpisodeFile>(x => x.SeriesId) .GroupBy<EpisodeFile>(x => x.SeasonNumber); } @@ -105,7 +109,9 @@ private SqlBuilder EpisodeFilesBuilder() .Select(@"""SeriesId"", ""SeasonNumber"", SUM(COALESCE(""Size"", 0)) AS SizeOnDisk, - string_agg(""ReleaseGroup"", '|') AS ReleaseGroupsString") + string_agg(DISTINCT ""ReleaseGroup"", '|') AS ReleaseGroupsString, + string_agg(DISTINCT ""ReleaseType""::text, '|') AS ReleaseTypesString, + string_agg(DISTINCT ""Quality""::json->>'quality', '|') AS EpisodeFileQualitiesString") .GroupBy<EpisodeFile>(x => x.SeriesId) .GroupBy<EpisodeFile>(x => x.SeasonNumber); } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs index 6e7b75d85..693eab6e1 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs @@ -1,31 +1,50 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.SeriesStats { public interface ISeriesStatisticsService { List<SeriesStatistics> SeriesStatistics(); - SeriesStatistics SeriesStatistics(int seriesId); + SeriesStatistics SeriesStatistics(int seriesId, int qualityProfileId); } public class SeriesStatisticsService : ISeriesStatisticsService { private readonly ISeriesStatisticsRepository _seriesStatisticsRepository; + private readonly ISeriesService _seriesService; + private readonly IQualityProfileService _qualityProfileService; - public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepository) + public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepository, + ISeriesService seriesService, + IQualityProfileService qualityProfileService) { _seriesStatisticsRepository = seriesStatisticsRepository; + _seriesService = seriesService; + _qualityProfileService = qualityProfileService; } public List<SeriesStatistics> SeriesStatistics() { var seasonStatistics = _seriesStatisticsRepository.SeriesStatistics(); + var seriesProfiles = _seriesService.GetAllSeriesQualityProfiles(); + var profiles = _qualityProfileService.All().ToDictionary(p => p.Id); - return seasonStatistics.GroupBy(s => s.SeriesId).Select(s => MapSeriesStatistics(s.ToList())).ToList(); + return seasonStatistics + .GroupBy(s => s.SeriesId) + .Select(s => + { + var profileId = seriesProfiles.GetValueOrDefault(s.Key); + profiles.TryGetValue(profileId, out var profile); + return MapSeriesStatistics(s.ToList(), profile); + }) + .ToList(); } - public SeriesStatistics SeriesStatistics(int seriesId) + public SeriesStatistics SeriesStatistics(int seriesId, int qualityProfileId) { var stats = _seriesStatisticsRepository.SeriesStatistics(seriesId); @@ -34,10 +53,12 @@ public SeriesStatistics SeriesStatistics(int seriesId) return new SeriesStatistics(); } - return MapSeriesStatistics(stats); + var profile = _qualityProfileService.Get(qualityProfileId); + + return MapSeriesStatistics(stats, profile); } - private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics) + private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics, QualityProfile profile) { var seriesStatistics = new SeriesStatistics { @@ -48,7 +69,9 @@ private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatis TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), MonitoredEpisodeCount = seasonStatistics.Sum(s => s.MonitoredEpisodeCount), SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk), - ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList() + ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList(), + ReleaseTypes = seasonStatistics.SelectMany(s => s.ReleaseTypes).Distinct().OrderBy(s => s).ToList(), + EpisodeFileQualities = SortQualities(seasonStatistics.SelectMany(s => s.EpisodeFileQualities).Distinct().ToList(), profile) }; var nextAiring = seasonStatistics.Where(s => s.NextAiring != null).MinBy(s => s.NextAiring); @@ -61,5 +84,15 @@ private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatis return seriesStatistics; } + + private static List<Quality> SortQualities(List<Quality> qualities, QualityProfile profile) + { + if (profile == null) + { + return qualities; + } + + return qualities.OrderBy(q => profile.GetIndex(q.Id).Index).ToList(); + } } } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 63946075c..075262c04 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -3,20 +3,20 @@ <TargetFrameworks>net10.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="Dapper" Version="2.1.66" /> + <PackageReference Include="Dapper" Version="2.1.72" /> <PackageReference Include="Diacritical.Net" Version="1.0.5" /> <PackageReference Include="Equ" Version="2.3.0" /> - <PackageReference Include="MailKit" Version="4.14.1" /> - <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="10.0.2" /> - <PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" /> + <PackageReference Include="MailKit" Version="4.16.0" /> + <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="10.0.7" /> + <PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" /> <PackageReference Include="MiniProfiler.AspNetCore" Version="4.5.4" /> <PackageReference Include="Openur.FFMpegCore" Version="5.4.0.31" /> - <PackageReference Include="Openur.FFprobeStatic" Version="8.0.1.302" /> - <PackageReference Include="Polly" Version="8.6.5" /> - <PackageReference Include="System.Drawing.Common" Version="10.0.2" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" /> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" /> + <PackageReference Include="Openur.FFprobeStatic" Version="8.1.0.334" /> + <PackageReference Include="Polly" Version="8.6.6" /> + <PackageReference Include="System.Drawing.Common" Version="10.0.7" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.7" /> <PackageReference Include="FluentMigrator.Runner.Core" Version="8.0.1" /> <PackageReference Include="FluentMigrator.Runner.SQLite" Version="8.0.1" /> <PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" /> @@ -25,7 +25,7 @@ <PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="NLog" Version="5.5.1" /> <PackageReference Include="MonoTorrent" Version="3.0.2" /> - <PackageReference Include="Npgsql" Version="10.0.1" /> + <PackageReference Include="Npgsql" Version="10.0.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index b28058150..79b4f9c85 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -27,7 +27,7 @@ public interface IEpisodeService List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber); List<Episode> GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber); List<Episode> EpisodesWithFiles(int seriesId); - PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec); + PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials); List<Episode> GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); @@ -158,11 +158,9 @@ public List<Episode> EpisodesWithFiles(int seriesId) return _episodeRepository.EpisodesWithFiles(seriesId); } - public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec) + public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials) { - var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, true); - - return episodeResult; + return _episodeRepository.EpisodesWithoutFiles(pagingSpec, includeSpecials); } public List<Episode> GetEpisodesByFileId(int episodeFileId) diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 8499ef261..f18f8e7f9 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -111,6 +111,7 @@ private Series RefreshSeriesInfo(int seriesId) series.Actors = seriesInfo.Actors; series.Genres = seriesInfo.Genres; series.Certification = seriesInfo.Certification; + series.OriginalCountry = seriesInfo.OriginalCountry; try { @@ -230,7 +231,7 @@ public void Execute(RefreshSeriesCommand message) _logger.Error("Series '{0}' (tvdbid {1}) was not found, it may have been removed from TheTVDB.", series.Title, series.TvdbId); // Mark the result as indeterminate so it's not marked as a full success, - // // but we can still process other series if needed. + // but we can still process other series if needed. _commandResultReporter.Report(CommandResult.Indeterminate); } catch (Exception e) diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 2464742b7..8c2017684 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -57,7 +57,7 @@ public Series() public DateTime? LastAired { get; set; } public LazyLoaded<QualityProfile> QualityProfile { get; set; } public Language OriginalLanguage { get; set; } - + public string OriginalCountry { get; set; } public List<Season> Seasons { get; set; } public HashSet<int> Tags { get; set; } public AddSeriesOptions AddOptions { get; set; } diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index f83545cc8..a4f6e6365 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -19,6 +19,7 @@ public interface ISeriesRepository : IBasicRepository<Series> List<int> AllSeriesTvdbIds(); Dictionary<int, string> AllSeriesPaths(); Dictionary<int, List<int>> AllSeriesTags(); + Dictionary<int, int> AllSeriesQualityProfiles(); } public class SeriesRepository : BasicRepository<Series>, ISeriesRepository @@ -111,6 +112,15 @@ public Dictionary<int, List<int>> AllSeriesTags() } } + public Dictionary<int, int> AllSeriesQualityProfiles() + { + using (var conn = _database.OpenConnection()) + { + var strSql = "SELECT \"Id\" AS Key, \"QualityProfileId\" AS Value FROM \"Series\""; + return conn.Query<KeyValuePair<int, int>>(strSql).ToDictionary(x => x.Key, x => x.Value); + } + } + private Series ReturnSingleSeriesOrThrow(List<Series> series) { if (series.Count == 0) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 29cb6fac5..2430cb759 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -28,6 +28,7 @@ public interface ISeriesService Dictionary<int, string> GetAllSeriesPaths(); Dictionary<int, List<int>> GetAllSeriesTags(); List<Series> AllForTag(int tagId); + Dictionary<int, int> GetAllSeriesQualityProfiles(); Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true); List<Series> UpdateSeries(List<Series> series, bool useExistingRelativeFolder); bool SeriesPathExists(string folder); @@ -186,6 +187,11 @@ public Dictionary<int, List<int>> GetAllSeriesTags() return _seriesRepository.AllSeriesTags(); } + public Dictionary<int, int> GetAllSeriesQualityProfiles() + { + return _seriesRepository.AllSeriesQualityProfiles(); + } + public List<Series> AllForTag(int tagId) { return GetAllSeries().Where(s => s.Tags.Contains(tagId)) diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index a75169147..670d95172 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -6,8 +6,8 @@ <ItemGroup> <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" /> - <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.2" /> + <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.7" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.7" /> <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> </ItemGroup> diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 85b1bb55e..51bb431ff 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -108,6 +108,11 @@ public void ConfigureServices(IServiceCollection services) }) .AddControllersAsServices(); + services.ConfigureHttpJsonOptions(options => + { + STJson.ApplySerializerSettings(options.SerializerOptions); + }); + services.AddSwaggerGen(c => { c.SwaggerDoc("v3", new OpenApiInfo diff --git a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj index eb5d56c27..f97d4cf00 100644 --- a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj @@ -4,7 +4,7 @@ <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.7" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> diff --git a/src/NzbDrone/Sonarr.csproj b/src/NzbDrone/Sonarr.csproj index 113ebaaa7..f9ce03ae5 100644 --- a/src/NzbDrone/Sonarr.csproj +++ b/src/NzbDrone/Sonarr.csproj @@ -8,7 +8,7 @@ <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources> </PropertyGroup> <ItemGroup> - <PackageReference Include="System.Resources.Extensions" Version="10.0.2" /> + <PackageReference Include="System.Resources.Extensions" Version="10.0.7" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" /> diff --git a/src/Sonarr.Api.V3/Health/HealthController.cs b/src/Sonarr.Api.V3/Health/HealthController.cs index fe1fd954c..10fbadcc0 100644 --- a/src/Sonarr.Api.V3/Health/HealthController.cs +++ b/src/Sonarr.Api.V3/Health/HealthController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; @@ -23,7 +24,7 @@ public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthChec } [NonAction] - public override ActionResult<HealthResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<HealthResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs b/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs index e08475952..78e30a989 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseControllerBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Profiles.Qualities; @@ -17,7 +18,7 @@ public ReleaseControllerBase(IQualityProfileService qualityProfileService) } [NonAction] - public override ActionResult<ReleaseResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<ReleaseResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileSchemaController.cs b/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileSchemaController.cs index ad5a32704..bccee3469 100644 --- a/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileSchemaController.cs +++ b/src/Sonarr.Api.V3/Profiles/Languages/LanguageProfileSchemaController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Languages; using Sonarr.Http; @@ -33,7 +34,7 @@ public LanguageProfileResource GetSchema() } [NonAction] - public override ActionResult<LanguageProfileResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<LanguageProfileResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs index f1ffc58b9..af0b44b7d 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileController.cs @@ -23,7 +23,7 @@ public ReleaseProfileController(IReleaseProfileService releaseProfileService, II SharedValidator.RuleFor(d => d).Custom((restriction, context) => { - if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty()) + if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty() && !restriction.AirDateRestriction) { context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required"); } diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs index 127f1b265..254f1a20a 100644 --- a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs @@ -15,6 +15,8 @@ public class ReleaseProfileResource : RestResource // Is List<string>, string or JArray, we accept 'string' with POST for backward compatibility public object Required { get; set; } public object Ignored { get; set; } + public bool AirDateRestriction { get; set; } + public int AirDateGracePeriod { get; set; } public int IndexerId { get; set; } public HashSet<int> Tags { get; set; } public HashSet<int> ExcludedTags { get; set; } @@ -42,6 +44,8 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model) Enabled = model.Enabled, Required = model.Required ?? new List<string>(), Ignored = model.Ignored ?? new List<string>(), + AirDateRestriction = model.AirDateRestriction, + AirDateGracePeriod = model.AirDateGracePeriod, IndexerId = model.IndexerIds.FirstOrDefault(0), Tags = new HashSet<int>(model.Tags), ExcludedTags = new HashSet<int>(model.ExcludedTags) @@ -62,6 +66,8 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource) Enabled = resource.Enabled, Required = resource.MapRequired(), Ignored = resource.MapIgnored(), + AirDateRestriction = resource.AirDateRestriction, + AirDateGracePeriod = resource.AirDateGracePeriod, IndexerIds = new List<int> { resource.IndexerId }, Tags = new HashSet<int>(resource.Tags), ExcludedTags = new HashSet<int>(resource.ExcludedTags) diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index 454b497fc..684d29a10 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Blocklisting; @@ -61,7 +62,7 @@ public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, } [NonAction] - public override ActionResult<QueueResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs b/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs index 152b4b733..d4d28afd9 100644 --- a/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueDetailsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; @@ -28,7 +29,7 @@ public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, } [NonAction] - public override ActionResult<QueueResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusController.cs b/src/Sonarr.Api.V3/Queue/QueueStatusController.cs index 57d87d107..fa43ad189 100644 --- a/src/Sonarr.Api.V3/Queue/QueueStatusController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueStatusController.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; @@ -31,7 +32,7 @@ public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, I } [NonAction] - public override ActionResult<QueueStatusResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueStatusResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs index 341373167..04a6abe82 100644 --- a/src/Sonarr.Api.V3/Series/SeriesController.cs +++ b/src/Sonarr.Api.V3/Series/SeriesController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.DataAugmentation.Scene; @@ -130,7 +131,7 @@ public List<SeriesResource> AllSeries(int? tvdbId, bool includeSeasonImages = fa } [NonAction] - public override ActionResult<SeriesResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<SeriesResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } @@ -237,7 +238,7 @@ private void MapCoversToLocal(params SeriesResource[] series) private void FetchAndLinkSeriesStatistics(SeriesResource resource) { - LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id)); + LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id, resource.QualityProfileId)); } private void LinkSeriesStatistics(List<SeriesResource> resources, Dictionary<int, SeriesStatistics> seriesStatistics) diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index f702a94a1..020e9cdff 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -6,7 +6,7 @@ <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Ical.Net" Version="4.3.1" /> <PackageReference Include="NLog" Version="5.5.1" /> - <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" /> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" /> diff --git a/src/Sonarr.Api.V3/Wanted/MissingController.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs index bbfde535f..ab867459d 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs @@ -48,7 +48,7 @@ public PagingResource<EpisodeResource> GetMissingEpisodes([FromQuery] PagingRequ pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false); } - var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages)); + var resource = pagingSpec.ApplyToPage(spec => _episodeService.EpisodesWithoutFiles(spec, true), v => MapToResource(v, includeSeries, false, includeImages)); return resource; } diff --git a/src/Sonarr.Api.V5/Blocklist/BlocklistController.cs b/src/Sonarr.Api.V5/Blocklist/BlocklistController.cs index 22b27d3cf..f4895e6b2 100644 --- a/src/Sonarr.Api.V5/Blocklist/BlocklistController.cs +++ b/src/Sonarr.Api.V5/Blocklist/BlocklistController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Blocklisting; using NzbDrone.Core.CustomFormats; @@ -24,7 +26,7 @@ public BlocklistController(IBlocklistService blocklistService, [HttpGet] [Produces("application/json")] - public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging, [FromQuery] int[]? seriesIds = null, [FromQuery] DownloadProtocol[]? protocols = null) + public Ok<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>( @@ -48,23 +50,23 @@ public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestR pagingSpec.FilterExpressions.Add(b => protocols.Contains(b.Protocol)); } - return pagingSpec.ApplyToPage(b => _blocklistService.Paged(pagingSpec), b => BlocklistResourceMapper.MapToResource(b, _formatCalculator)); + return TypedResults.Ok(pagingSpec.ApplyToPage(b => _blocklistService.Paged(pagingSpec), b => BlocklistResourceMapper.MapToResource(b, _formatCalculator))); } [RestDeleteById] - public ActionResult DeleteBlocklist(int id) + public NoContent DeleteBlocklist(int id) { _blocklistService.Delete(id); - return NoContent(); + return TypedResults.NoContent(); } [HttpDelete("bulk")] [Produces("application/json")] - public ActionResult Remove([FromBody] BlocklistBulkResource resource) + public NoContent Remove([FromBody] BlocklistBulkResource resource) { _blocklistService.Delete(resource.Ids); - return NoContent(); + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/Calendar/CalendarController.cs b/src/Sonarr.Api.V5/Calendar/CalendarController.cs index 3eb37d4da..003c60d83 100644 --- a/src/Sonarr.Api.V5/Calendar/CalendarController.cs +++ b/src/Sonarr.Api.V5/Calendar/CalendarController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; @@ -28,7 +30,7 @@ public CalendarController(IBroadcastSignalRMessage signalR, [HttpGet] [Produces("application/json")] - public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool includeUnmonitored = false, bool includeSpecials = true, string tags = "", [FromQuery] CalendarSubresource[]? includeSubresources = null) + public Ok<List<EpisodeResource>> GetCalendar(DateTime? start, DateTime? end, bool includeUnmonitored = false, bool includeSpecials = true, string tags = "", [FromQuery] CalendarSubresource[]? includeSubresources = null) { var startUse = start ?? DateTime.Today; var endUse = end ?? DateTime.Today.AddDays(2); @@ -65,7 +67,7 @@ public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool in var resources = MapToResource(result, includeSeries, includeEpisodeFile, includeEpisodeImages); - return resources.OrderBy(e => e.AirDateUtc).ToList(); + return TypedResults.Ok(resources.OrderBy(e => e.AirDateUtc).ToList()); } } } diff --git a/src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs b/src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs index e1f1ec083..c8c0b4095 100644 --- a/src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs +++ b/src/Sonarr.Api.V5/Calendar/CalendarFeedController.cs @@ -2,6 +2,8 @@ using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tags; @@ -25,7 +27,7 @@ public CalendarFeedController(IEpisodeService episodeService, ISeriesService ser } [HttpGet("Sonarr.ics")] - public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tags = "", bool unmonitored = false, bool premieresOnly = false, bool asAllDay = false, bool includeSpecials = true) + public ContentHttpResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, string tags = "", bool unmonitored = false, bool premieresOnly = false, bool asAllDay = false, bool includeSpecials = true) { var start = DateTime.Today.AddDays(-pastDays); var end = DateTime.Today.AddDays(futureDays); @@ -96,6 +98,6 @@ public IActionResult GetCalendarFeed(int pastDays = 7, int futureDays = 28, stri var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); var icalendar = serializer.SerializeToString(calendar); - return Content(icalendar, "text/calendar"); + return TypedResults.Content(icalendar, "text/calendar"); } } diff --git a/src/Sonarr.Api.V5/Commands/CommandController.cs b/src/Sonarr.Api.V5/Commands/CommandController.cs index ebb0b3beb..2880f3066 100644 --- a/src/Sonarr.Api.V5/Commands/CommandController.cs +++ b/src/Sonarr.Api.V5/Commands/CommandController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Composition; using NzbDrone.Common.Serializer; @@ -46,7 +48,7 @@ protected override CommandResource GetResourceById(int id) [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource) + public Results<Created<CommandResource>, NotFound> StartCommand([FromBody] CommandResource commandResource) { var commandType = _knownTypes.GetImplementations(typeof(Command)) @@ -70,24 +72,26 @@ public ActionResult<CommandResource> StartCommand([FromBody] CommandResource com var trackedCommand = _commandQueueManager.Push(command, commandResource.Priority, CommandTrigger.Manual); - return Created(trackedCommand.Id); + return TypedCreated(trackedCommand.Id); } } [HttpGet] [Produces("application/json")] - public List<CommandResource> GetStartedCommands() + public Ok<List<CommandResource>> GetStartedCommands() { - return _commandQueueManager.All() + return TypedResults.Ok(_commandQueueManager.All() .OrderBy(c => c.Status, _commandPriorityComparer) .ThenByDescending(c => c.Priority) - .ToResource(); + .ToResource()); } [RestDeleteById] - public void CancelCommand(int id) + public NoContent CancelCommand(int id) { _commandQueueManager.Cancel(id); + + return TypedResults.NoContent(); } [NonAction] diff --git a/src/Sonarr.Api.V5/Connections/ConnectionController.cs b/src/Sonarr.Api.V5/Connections/ConnectionController.cs index 5349a6d8b..c5dc47d1f 100644 --- a/src/Sonarr.Api.V5/Connections/ConnectionController.cs +++ b/src/Sonarr.Api.V5/Connections/ConnectionController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Notifications; using NzbDrone.SignalR; @@ -18,13 +19,13 @@ public ConnectionController(IBroadcastSignalRMessage signalRBroadcaster, Notific } [NonAction] - public override ActionResult<ConnectionResource> UpdateProvider([FromBody] ConnectionBulkResource providerResource) + public override Results<Ok<IEnumerable<ConnectionResource>>, BadRequest> UpdateProvider([FromBody] ConnectionBulkResource providerResource) { throw new NotImplementedException(); } [NonAction] - public override ActionResult DeleteProviders([FromBody] ConnectionBulkResource resource) + public override NoContent DeleteProviders([FromBody] ConnectionBulkResource resource) { throw new NotImplementedException(); } diff --git a/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs b/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs index 61a267b68..6cfe21944 100644 --- a/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs +++ b/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFilters; using Sonarr.Http; @@ -23,33 +25,33 @@ protected override CustomFilterResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<CustomFilterResource> GetCustomFilters() + public Ok<List<CustomFilterResource>> GetCustomFilters() { - return _customFilterService.All().ToResource(); + return TypedResults.Ok(_customFilterService.All().ToResource()); } [RestPostById] [Consumes("application/json")] - public ActionResult<CustomFilterResource> AddCustomFilter([FromBody] CustomFilterResource resource) + public Results<Created<CustomFilterResource>, NotFound> AddCustomFilter([FromBody] CustomFilterResource resource) { var customFilter = _customFilterService.Add(resource.ToModel()); - return Created(customFilter.Id); + return TypedCreated(customFilter.Id); } [RestPutById] [Consumes("application/json")] - public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFilterResource resource) + public Results<Accepted<CustomFilterResource>, NotFound> UpdateCustomFilter([FromBody] CustomFilterResource resource) { _customFilterService.Update(resource.ToModel()); - return Accepted(resource.Id); + return TypedAccepted(resource.Id); } [RestDeleteById] - public ActionResult DeleteCustomResource(int id) + public NoContent DeleteCustomResource(int id) { _customFilterService.Delete(id); - return NoContent(); + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/DiskSpace/DiskSpaceController.cs b/src/Sonarr.Api.V5/DiskSpace/DiskSpaceController.cs index b0a1a9abe..04cc40421 100644 --- a/src/Sonarr.Api.V5/DiskSpace/DiskSpaceController.cs +++ b/src/Sonarr.Api.V5/DiskSpace/DiskSpaceController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.DiskSpace; using Sonarr.Http; @@ -16,8 +18,8 @@ public DiskSpaceController(IDiskSpaceService diskSpaceService) [HttpGet] [Produces("application/json")] - public List<DiskSpaceResource> GetFreeSpace() + public Ok<List<DiskSpaceResource>> GetFreeSpace() { - return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); + return TypedResults.Ok(_diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource)); } } diff --git a/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs index aed000529..921b91682 100644 --- a/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V5/EpisodeFiles/EpisodeFileController.cs @@ -1,4 +1,6 @@ using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore.Events; @@ -57,7 +59,7 @@ protected override EpisodeFileResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<EpisodeFileResource> GetEpisodeFiles(int? seriesId, [FromQuery] List<int>? episodeFileIds) + public Results<Ok<List<EpisodeFileResource>>, BadRequest> GetEpisodeFiles(int? seriesId, [FromQuery] List<int>? episodeFileIds) { if (!seriesId.HasValue && episodeFileIds?.Any() == false) { @@ -71,25 +73,25 @@ public List<EpisodeFileResource> GetEpisodeFiles(int? seriesId, [FromQuery] List if (files == null) { - return new List<EpisodeFileResource>(); + return TypedResults.Ok(new List<EpisodeFileResource>()); } - return files.ConvertAll(e => e.ToResource(series, _upgradableSpecification, _formatCalculator)); + return TypedResults.Ok(files.ConvertAll(e => e.ToResource(series, _upgradableSpecification, _formatCalculator))); } else { var episodeFiles = _mediaFileService.Get(episodeFileIds); - return episodeFiles.GroupBy(e => e.SeriesId) + return TypedResults.Ok(episodeFiles.GroupBy(e => e.SeriesId) .SelectMany(f => f.ToList() .ConvertAll(e => e.ToResource(_seriesService.GetSeries(f.Key), _upgradableSpecification, _formatCalculator))) - .ToList(); + .ToList()); } } [RestPutById] [Consumes("application/json")] - public ActionResult<EpisodeFileResource> SetQuality([FromBody] EpisodeFileResource episodeFileResource) + public Results<Accepted<EpisodeFileResource>, NotFound> SetQuality([FromBody] EpisodeFileResource episodeFileResource) { var episodeFile = _mediaFileService.Get(episodeFileResource.Id); episodeFile.Quality = episodeFileResource.Quality; @@ -105,11 +107,11 @@ public ActionResult<EpisodeFileResource> SetQuality([FromBody] EpisodeFileResour } _mediaFileService.Update(episodeFile); - return Accepted(episodeFile.Id); + return TypedAccepted(episodeFile.Id); } [RestDeleteById] - public void DeleteEpisodeFile(int id) + public Results<NoContent, NotFound> DeleteEpisodeFile(int id) { var episodeFile = _mediaFileService.Get(id); @@ -121,11 +123,13 @@ public void DeleteEpisodeFile(int id) var series = _seriesService.GetSeries(episodeFile.SeriesId); _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); + + return TypedResults.NoContent(); } [HttpDelete("bulk")] [Consumes("application/json")] - public object DeleteEpisodeFiles([FromBody] EpisodeFileListResource resource) + public NoContent DeleteEpisodeFiles([FromBody] EpisodeFileListResource resource) { var episodeFiles = _mediaFileService.GetFiles(resource.EpisodeFileIds); var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); @@ -135,12 +139,12 @@ public object DeleteEpisodeFiles([FromBody] EpisodeFileListResource resource) _mediaFileDeletionService.DeleteEpisodeFile(series, episodeFile); } - return new { }; + return TypedResults.NoContent(); } [HttpPut("bulk")] [Consumes("application/json")] - public object SetPropertiesBulk([FromBody] List<EpisodeFileResource> resources) + public Ok<List<EpisodeFileResource>> SetPropertiesBulk([FromBody] List<EpisodeFileResource> resources) { var episodeFiles = _mediaFileService.GetFiles(resources.Select(r => r.Id)); @@ -184,7 +188,7 @@ public object SetPropertiesBulk([FromBody] List<EpisodeFileResource> resources) var series = _seriesService.GetSeries(episodeFiles.First().SeriesId); - return Accepted(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification, _formatCalculator))); + return TypedResults.Ok(episodeFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification, _formatCalculator))); } [NonAction] diff --git a/src/Sonarr.Api.V5/Episodes/EpisodeController.cs b/src/Sonarr.Api.V5/Episodes/EpisodeController.cs index bb6e105ef..367bd1aca 100644 --- a/src/Sonarr.Api.V5/Episodes/EpisodeController.cs +++ b/src/Sonarr.Api.V5/Episodes/EpisodeController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine.Specifications; @@ -23,7 +25,7 @@ public EpisodeController(ISeriesService seriesService, [HttpGet] [Produces("application/json")] - public List<EpisodeResource> GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List<int> episodeIds, int? episodeFileId, [FromQuery] EpisodeSubresource[]? includeSubresources = null) + public Results<Ok<List<EpisodeResource>>, BadRequest> GetEpisodes(int? seriesId, int? seasonNumber, [FromQuery]List<int> episodeIds, int? episodeFileId, [FromQuery] EpisodeSubresource[]? includeSubresources = null) { var includeSeries = includeSubresources.Contains(EpisodeSubresource.Series); var includeEpisodeFile = includeSubresources.Contains(EpisodeSubresource.EpisodeFile); @@ -33,18 +35,18 @@ public List<EpisodeResource> GetEpisodes(int? seriesId, int? seasonNumber, [From { if (seasonNumber.HasValue) { - return MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), includeSeries, includeEpisodeFile, includeImages); + return TypedResults.Ok(MapToResource(_episodeService.GetEpisodesBySeason(seriesId.Value, seasonNumber.Value), includeSeries, includeEpisodeFile, includeImages)); } - return MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), includeSeries, includeEpisodeFile, includeImages); + return TypedResults.Ok(MapToResource(_episodeService.GetEpisodeBySeries(seriesId.Value), includeSeries, includeEpisodeFile, includeImages)); } else if (episodeIds.Any()) { - return MapToResource(_episodeService.GetEpisodes(episodeIds), includeSeries, includeEpisodeFile, includeImages); + return TypedResults.Ok(MapToResource(_episodeService.GetEpisodes(episodeIds), includeSeries, includeEpisodeFile, includeImages)); } else if (episodeFileId.HasValue) { - return MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), includeSeries, includeEpisodeFile, includeImages); + return TypedResults.Ok(MapToResource(_episodeService.GetEpisodesByFileId(episodeFileId.Value), includeSeries, includeEpisodeFile, includeImages)); } throw new BadRequestException("seriesId or episodeIds must be provided"); @@ -52,18 +54,18 @@ public List<EpisodeResource> GetEpisodes(int? seriesId, int? seasonNumber, [From [RestPutById] [Consumes("application/json")] - public ActionResult<EpisodeResource> SetEpisodeMonitored([FromRoute] int id, [FromBody] EpisodeResource resource) + public Ok<EpisodeResource> SetEpisodeMonitored([FromRoute] int id, [FromBody] EpisodeResource resource) { _episodeService.SetEpisodeMonitored(id, resource.Monitored); resource = MapToResource(_episodeService.GetEpisode(id), false, false, false); - return Accepted(resource); + return TypedResults.Ok(resource); } [HttpPut("monitor")] [Consumes("application/json")] - public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource, [FromQuery] EpisodeSubresource[]? includeSubresources = null) + public Ok<List<EpisodeResource>> SetEpisodesMonitored([FromBody] EpisodesMonitoredResource resource, [FromQuery] EpisodeSubresource[]? includeSubresources = null) { var includeImages = includeSubresources.Contains(EpisodeSubresource.Images); @@ -78,6 +80,6 @@ public IActionResult SetEpisodesMonitored([FromBody] EpisodesMonitoredResource r var resources = MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages); - return Accepted(resources); + return TypedResults.Ok(resources); } } diff --git a/src/Sonarr.Api.V5/Episodes/RenameEpisodeController.cs b/src/Sonarr.Api.V5/Episodes/RenameEpisodeController.cs index 05494a33b..7c8e5ccdb 100644 --- a/src/Sonarr.Api.V5/Episodes/RenameEpisodeController.cs +++ b/src/Sonarr.Api.V5/Episodes/RenameEpisodeController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.MediaFiles; using Sonarr.Http; @@ -17,19 +19,19 @@ public RenameEpisodeController(IRenameEpisodeFileService renameEpisodeFileServic [HttpGet] [Produces("application/json")] - public List<RenameEpisodeResource> GetEpisodes(int seriesId, int? seasonNumber) + public Ok<List<RenameEpisodeResource>> GetEpisodes(int seriesId, int? seasonNumber) { if (seasonNumber.HasValue) { - return _renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber.Value).ToResource(); + return TypedResults.Ok(_renameEpisodeFileService.GetRenamePreviews(seriesId, seasonNumber.Value).ToResource()); } - return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource(); + return TypedResults.Ok(_renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource()); } [HttpGet("bulk")] [Produces("application/json")] - public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds) + public Results<Ok<List<RenameEpisodeResource>>, BadRequest> GetEpisodes([FromQuery] List<int> seriesIds) { if (seriesIds is { Count: 0 }) { @@ -41,6 +43,6 @@ public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds) throw new BadRequestException("seriesIds must be positive integers"); } - return _renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource(); + return TypedResults.Ok(_renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource()); } } diff --git a/src/Sonarr.Api.V5/FileSystem/FileSystemController.cs b/src/Sonarr.Api.V5/FileSystem/FileSystemController.cs index 2c52ca800..820b41810 100644 --- a/src/Sonarr.Api.V5/FileSystem/FileSystemController.cs +++ b/src/Sonarr.Api.V5/FileSystem/FileSystemController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -24,38 +26,38 @@ public FileSystemController(IFileSystemLookupService fileSystemLookupService, [HttpGet] [Produces("application/json")] - public IActionResult GetContents(string path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false) + public Ok<FileSystemResult> GetContents(string? path, bool includeFiles = false, bool allowFoldersWithoutTrailingSlashes = false) { - return Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes)); + return TypedResults.Ok(_fileSystemLookupService.LookupContents(path, includeFiles, allowFoldersWithoutTrailingSlashes)); } [HttpGet("type")] [Produces("application/json")] - public object GetEntityType(string path) + public Ok<object> GetEntityType(string path) { if (_diskProvider.FileExists(path)) { - return new { type = "file" }; + return TypedResults.Ok((object)new { type = "file" }); } // Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system - return new { type = "folder" }; + return TypedResults.Ok((object)new { type = "folder" }); } [HttpGet("mediafiles")] [Produces("application/json")] - public object GetMediaFiles(string path) + public Ok<IEnumerable<object>> GetMediaFiles(string path) { if (!_diskProvider.FolderExists(path)) { - return Array.Empty<string>(); + return TypedResults.Ok(Enumerable.Empty<object>()); } - return _diskScanService.GetVideoFiles(path).Select(f => new + return TypedResults.Ok(_diskScanService.GetVideoFiles(path).Select(object (f) => new { Path = f, RelativePath = path.GetRelativePath(f), Name = Path.GetFileName(f) - }); + })); } } diff --git a/src/Sonarr.Api.V5/Health/HealthController.cs b/src/Sonarr.Api.V5/Health/HealthController.cs index f7912fe7d..4610a1c5e 100644 --- a/src/Sonarr.Api.V5/Health/HealthController.cs +++ b/src/Sonarr.Api.V5/Health/HealthController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; @@ -21,7 +23,7 @@ public HealthController(IBroadcastSignalRMessage signalRBroadcaster, IHealthChec } [NonAction] - public override ActionResult<HealthResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<HealthResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } @@ -33,9 +35,9 @@ protected override HealthResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<HealthResource> GetHealth() + public Ok<List<HealthResource>> GetHealth() { - return _healthCheckService.Results().ToResource(); + return TypedResults.Ok(_healthCheckService.Results().ToResource()); } [NonAction] diff --git a/src/Sonarr.Api.V5/History/HistoryController.cs b/src/Sonarr.Api.V5/History/HistoryController.cs index 38d805009..f11b960d0 100644 --- a/src/Sonarr.Api.V5/History/HistoryController.cs +++ b/src/Sonarr.Api.V5/History/HistoryController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; @@ -62,7 +64,7 @@ protected HistoryResource MapToResource(EpisodeHistory model, bool includeSeries [HttpGet] [Produces("application/json")] - public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[]? eventTypes, int? episodeId, string? downloadId, [FromQuery] int[]? seriesIds = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] HistorySubresource[]? includeSubresources = null) + public Ok<PagingResource<HistoryResource>> GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[]? eventTypes, int? episodeId, string? downloadId, [FromQuery] int[]? seriesIds = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var pagingResource = new PagingResource<HistoryResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EpisodeHistory>( @@ -97,74 +99,74 @@ public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResou var includeSeries = includeSubresources.Contains(HistorySubresource.Series); var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); - return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeSeries, includeEpisode)); + return TypedResults.Ok(pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeSeries, includeEpisode))); } [HttpGet("since")] [Produces("application/json")] - public List<HistoryResource> GetHistorySince(DateTime date, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) + public Ok<List<HistoryResource>> GetHistorySince(DateTime date, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var includeSeries = includeSubresources.Contains(HistorySubresource.Series); var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); - return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList(); + return TypedResults.Ok(_historyService.Since(date, eventType).Select(h => MapToResource(h, includeSeries, includeEpisode)).ToList()); } [HttpGet("series")] [Produces("application/json")] - public List<HistoryResource> GetSeriesHistory(int seriesId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) + public Ok<List<HistoryResource>> GetSeriesHistory(int seriesId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var series = _seriesService.GetSeries(seriesId); var includeSeries = includeSubresources.Contains(HistorySubresource.Series); var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); - return _historyService.GetBySeries(seriesId, eventType).Select(h => + return TypedResults.Ok(_historyService.GetBySeries(seriesId, eventType).Select(h => { h.Series = series; return MapToResource(h, includeSeries, includeEpisode); - }).ToList(); + }).ToList()); } [HttpGet("season")] [Produces("application/json")] - public List<HistoryResource> GetSeasonHistory(int seriesId, int seasonNumber, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) + public Ok<List<HistoryResource>> GetSeasonHistory(int seriesId, int seasonNumber, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var series = _seriesService.GetSeries(seriesId); var includeSeries = includeSubresources.Contains(HistorySubresource.Series); var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); - return _historyService.GetBySeason(seriesId, seasonNumber, eventType).Select(h => + return TypedResults.Ok(_historyService.GetBySeason(seriesId, seasonNumber, eventType).Select(h => { h.Series = series; return MapToResource(h, includeSeries, includeEpisode); - }).ToList(); + }).ToList()); } [HttpGet("episode")] [Produces("application/json")] - public List<HistoryResource> GetEpisodeHistory(int episodeId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) + public Ok<List<HistoryResource>> GetEpisodeHistory(int episodeId, EpisodeHistoryEventType? eventType = null, [FromQuery] HistorySubresource[]? includeSubresources = null) { var episode = _episodeService.GetEpisode(episodeId); var series = _seriesService.GetSeries(episode.SeriesId); var includeSeries = includeSubresources.Contains(HistorySubresource.Series); var includeEpisode = includeSubresources.Contains(HistorySubresource.Episode); - return _historyService.GetByEpisode(episodeId, eventType) + return TypedResults.Ok(_historyService.GetByEpisode(episodeId, eventType) .Select(h => { h.Series = series; h.Episode = episode; return MapToResource(h, includeSeries, includeEpisode); - }).ToList(); + }).ToList()); } [HttpPost("failed/{id}")] - public ActionResult MarkAsFailed([FromRoute] int id) + public NoContent MarkAsFailed([FromRoute] int id) { _failedDownloadService.MarkAsFailed(id); - return NoContent(); + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/ImportLists/ImportListExclusionBulkResource.cs b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionBulkResource.cs new file mode 100644 index 000000000..667d970fb --- /dev/null +++ b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionBulkResource.cs @@ -0,0 +1,6 @@ +namespace Sonarr.Api.V5.ImportLists; + +public class ImportListExclusionBulkResource +{ + public required HashSet<int> Ids { get; set; } +} diff --git a/src/Sonarr.Api.V5/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionController.cs new file mode 100644 index 000000000..afae596c0 --- /dev/null +++ b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionController.cs @@ -0,0 +1,88 @@ +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.ImportLists.Exclusions; +using Sonarr.Http; +using Sonarr.Http.Extensions; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V5.ImportLists; + +[V5ApiController] +public class ImportListExclusionController : RestController<ImportListExclusionResource> +{ + private readonly IImportListExclusionService _importListExclusionService; + + public ImportListExclusionController(IImportListExclusionService importListExclusionService, + ImportListExclusionExistsValidator importListExclusionExistsValidator) + { + _importListExclusionService = importListExclusionService; + + SharedValidator.RuleFor(c => c.TvdbId).Cascade(CascadeMode.Stop) + .NotEmpty() + .SetValidator(importListExclusionExistsValidator); + + SharedValidator.RuleFor(c => c.Title).NotEmpty(); + } + + protected override ImportListExclusionResource GetResourceById(int id) + { + return _importListExclusionService.Get(id).ToResource(); + } + + [HttpGet] + [Produces("application/json")] + public Ok<PagingResource<ImportListExclusionResource>> GetImportListExclusions([FromQuery] PagingRequestResource paging) + { + var pagingResource = new PagingResource<ImportListExclusionResource>(paging); + var pageSpec = pagingResource.MapToPagingSpec<ImportListExclusionResource, ImportListExclusion>( + new HashSet<string>(StringComparer.OrdinalIgnoreCase) + { + "id", + "title", + "tvdbId" + }, + "id", + SortDirection.Descending); + + return TypedResults.Ok(pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource)); + } + + [RestPostById] + [Consumes("application/json")] + public Results<Created<ImportListExclusionResource>, NotFound> AddImportListExclusion([FromBody] ImportListExclusionResource resource) + { + var importListExclusion = _importListExclusionService.Add(resource.ToModel()); + + return TypedCreated(importListExclusion.Id); + } + + [RestPutById] + [Consumes("application/json")] + public Results<Accepted<ImportListExclusionResource>, NotFound> UpdateImportListExclusion([FromBody] ImportListExclusionResource resource) + { + _importListExclusionService.Update(resource.ToModel()); + + return TypedAccepted(resource.Id); + } + + [RestDeleteById] + public NoContent DeleteImportListExclusion(int id) + { + _importListExclusionService.Delete(id); + + return TypedResults.NoContent(); + } + + [HttpDelete("bulk")] + [Consumes("application/json")] + public NoContent DeleteImportListExclusions([FromBody] ImportListExclusionBulkResource resource) + { + _importListExclusionService.Delete(resource.Ids.ToList()); + + return TypedResults.NoContent(); + } +} diff --git a/src/Sonarr.Api.V5/ImportLists/ImportListExclusionExistsValidator.cs b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionExistsValidator.cs new file mode 100644 index 000000000..4b06274cc --- /dev/null +++ b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionExistsValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation.Validators; +using NzbDrone.Core.ImportLists.Exclusions; + +namespace Sonarr.Api.V5.ImportLists; + +public class ImportListExclusionExistsValidator : PropertyValidator +{ + private readonly IImportListExclusionService _importListExclusionService; + + public ImportListExclusionExistsValidator(IImportListExclusionService importListExclusionService) + { + _importListExclusionService = importListExclusionService; + } + + protected override string GetDefaultMessageTemplate() => "This exclusion has already been added."; + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) + { + return true; + } + + if (context.InstanceToValidate is not ImportListExclusionResource listExclusionResource) + { + return true; + } + + return !_importListExclusionService.All().Exists(v => v.TvdbId == (int)context.PropertyValue && v.Id != listExclusionResource.Id); + } +} diff --git a/src/Sonarr.Api.V5/ImportLists/ImportListExclusionResource.cs b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionResource.cs new file mode 100644 index 000000000..e3a2a6aa5 --- /dev/null +++ b/src/Sonarr.Api.V5/ImportLists/ImportListExclusionResource.cs @@ -0,0 +1,38 @@ +using NzbDrone.Core.ImportLists.Exclusions; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.ImportLists; + +public class ImportListExclusionResource : RestResource +{ + public int TvdbId { get; set; } + public string? Title { get; set; } +} + +public static class ImportListExclusionResourceMapper +{ + public static ImportListExclusionResource ToResource(this ImportListExclusion model) + { + return new ImportListExclusionResource + { + Id = model.Id, + TvdbId = model.TvdbId, + Title = model.Title, + }; + } + + public static ImportListExclusion ToModel(this ImportListExclusionResource resource) + { + return new ImportListExclusion + { + Id = resource.Id, + TvdbId = resource.TvdbId, + Title = resource.Title + }; + } + + public static List<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> models) + { + return models.Select(ToResource).ToList(); + } +} diff --git a/src/Sonarr.Api.V5/Indexers/IndexerBulkResource.cs b/src/Sonarr.Api.V5/Indexers/IndexerBulkResource.cs new file mode 100644 index 000000000..236387d66 --- /dev/null +++ b/src/Sonarr.Api.V5/Indexers/IndexerBulkResource.cs @@ -0,0 +1,30 @@ +using NzbDrone.Core.Indexers; +using Sonarr.Api.V5.Provider; + +namespace Sonarr.Api.V5.Indexers; + +public class IndexerBulkResource : ProviderBulkResource<IndexerBulkResource> +{ + public bool? EnableRss { get; set; } + public bool? EnableAutomaticSearch { get; set; } + public bool? EnableInteractiveSearch { get; set; } + public int? Priority { get; set; } + public int? SeasonSearchMaximumSingleEpisodeAge { get; set; } +} + +public class IndexerBulkResourceMapper : ProviderBulkResourceMapper<IndexerBulkResource, IndexerDefinition> +{ + public override List<IndexerDefinition> UpdateModel(IndexerBulkResource resource, List<IndexerDefinition> existingDefinitions) + { + existingDefinitions.ForEach(existing => + { + existing.EnableRss = resource.EnableRss ?? existing.EnableRss; + existing.EnableAutomaticSearch = resource.EnableAutomaticSearch ?? existing.EnableAutomaticSearch; + existing.EnableInteractiveSearch = resource.EnableInteractiveSearch ?? existing.EnableInteractiveSearch; + existing.Priority = resource.Priority ?? existing.Priority; + existing.SeasonSearchMaximumSingleEpisodeAge = resource.SeasonSearchMaximumSingleEpisodeAge ?? existing.SeasonSearchMaximumSingleEpisodeAge; + }); + + return existingDefinitions; + } +} diff --git a/src/Sonarr.Api.V5/Indexers/IndexerController.cs b/src/Sonarr.Api.V5/Indexers/IndexerController.cs new file mode 100644 index 000000000..82527fc54 --- /dev/null +++ b/src/Sonarr.Api.V5/Indexers/IndexerController.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Validation; +using NzbDrone.SignalR; +using Sonarr.Api.V5.Provider; +using Sonarr.Http; + +namespace Sonarr.Api.V5.Indexers; + +[V5ApiController] +public class IndexerController : ProviderControllerBase<IndexerResource, IndexerBulkResource, IIndexer, IndexerDefinition> +{ + public static readonly IndexerResourceMapper ResourceMapper = new(); + public static readonly IndexerBulkResourceMapper BulkResourceMapper = new(); + + public IndexerController(IBroadcastSignalRMessage signalRBroadcaster, + IndexerFactory indexerFactory, + DownloadClientExistsValidator downloadClientExistsValidator) + : base(signalRBroadcaster, indexerFactory, "indexer", ResourceMapper, BulkResourceMapper) + { + SharedValidator.RuleFor(c => c.Priority).InclusiveBetween(1, 50); + SharedValidator.RuleFor(c => c.SeasonSearchMaximumSingleEpisodeAge).GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(c => c.DownloadClientId).SetValidator(downloadClientExistsValidator); + } +} diff --git a/src/Sonarr.Api.V5/Indexers/IndexerFlagController.cs b/src/Sonarr.Api.V5/Indexers/IndexerFlagController.cs new file mode 100644 index 000000000..79d874e0c --- /dev/null +++ b/src/Sonarr.Api.V5/Indexers/IndexerFlagController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Parser.Model; +using Sonarr.Http; + +namespace Sonarr.Api.V5.Indexers; + +[V5ApiController] +public class IndexerFlagController : Controller +{ + [HttpGet] + public Ok<List<IndexerFlagResource>> GetAll() + { + return TypedResults.Ok(Enum.GetValues(typeof(IndexerFlags)).Cast<IndexerFlags>().Select(f => new IndexerFlagResource + { + Id = (int)f, + Name = f.ToString() + }).ToList()); + } +} diff --git a/src/Sonarr.Api.V5/Indexers/IndexerFlagResource.cs b/src/Sonarr.Api.V5/Indexers/IndexerFlagResource.cs new file mode 100644 index 000000000..925f7be62 --- /dev/null +++ b/src/Sonarr.Api.V5/Indexers/IndexerFlagResource.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.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.V5/Indexers/IndexerResource.cs b/src/Sonarr.Api.V5/Indexers/IndexerResource.cs new file mode 100644 index 000000000..e97a69470 --- /dev/null +++ b/src/Sonarr.Api.V5/Indexers/IndexerResource.cs @@ -0,0 +1,51 @@ +using NzbDrone.Core.Indexers; +using Sonarr.Api.V5.Provider; + +namespace Sonarr.Api.V5.Indexers; + +public class IndexerResource : ProviderResource<IndexerResource> +{ + public bool EnableRss { get; set; } + public bool EnableAutomaticSearch { get; set; } + public bool EnableInteractiveSearch { get; set; } + public bool SupportsRss { get; set; } + public bool SupportsSearch { get; set; } + public DownloadProtocol Protocol { get; set; } + public int Priority { get; set; } + public int SeasonSearchMaximumSingleEpisodeAge { get; set; } + public int DownloadClientId { get; set; } +} + +public class IndexerResourceMapper : ProviderResourceMapper<IndexerResource, IndexerDefinition> +{ + public override IndexerResource ToResource(IndexerDefinition definition) + { + var resource = base.ToResource(definition); + + resource.EnableRss = definition.EnableRss; + resource.EnableAutomaticSearch = definition.EnableAutomaticSearch; + resource.EnableInteractiveSearch = definition.EnableInteractiveSearch; + resource.SupportsRss = definition.SupportsRss; + resource.SupportsSearch = definition.SupportsSearch; + resource.Protocol = definition.Protocol; + resource.Priority = definition.Priority; + resource.SeasonSearchMaximumSingleEpisodeAge = definition.SeasonSearchMaximumSingleEpisodeAge; + resource.DownloadClientId = definition.DownloadClientId; + + return resource; + } + + public override IndexerDefinition ToModel(IndexerResource resource, IndexerDefinition? existingDefinition) + { + var definition = base.ToModel(resource, existingDefinition); + + definition.EnableRss = resource.EnableRss; + definition.EnableAutomaticSearch = resource.EnableAutomaticSearch; + definition.EnableInteractiveSearch = resource.EnableInteractiveSearch; + definition.Priority = resource.Priority; + definition.SeasonSearchMaximumSingleEpisodeAge = resource.SeasonSearchMaximumSingleEpisodeAge; + definition.DownloadClientId = resource.DownloadClientId; + + return definition; + } +} diff --git a/src/Sonarr.Api.V5/Localization/LanguageController.cs b/src/Sonarr.Api.V5/Localization/LanguageController.cs new file mode 100644 index 000000000..ddb5d188c --- /dev/null +++ b/src/Sonarr.Api.V5/Localization/LanguageController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Languages; +using Sonarr.Http; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.Localization; + +[V5ApiController] +public class LanguageController : RestController<LanguageResource> +{ + protected override LanguageResource GetResourceById(int id) + { + var language = (Language)id; + + return new LanguageResource + { + Id = (int)language, + Name = language.ToString() + }; + } + + [HttpGet] + public Ok<List<LanguageResource>> GetAll() + { + var languageResources = Language.All.Select(l => new LanguageResource + { + Id = (int)l, + Name = l.ToString() + }) + .OrderBy(l => l.Id > 0).ThenBy(l => l.Name) + .ToList(); + + return TypedResults.Ok(languageResources); + } +} diff --git a/src/Sonarr.Api.V5/Localization/LanguageResource.cs b/src/Sonarr.Api.V5/Localization/LanguageResource.cs new file mode 100644 index 000000000..1b3719b76 --- /dev/null +++ b/src/Sonarr.Api.V5/Localization/LanguageResource.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.Localization; + +public class LanguageResource : RestResource +{ + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public new int Id { get; set; } + public string? Name { get; set; } + public string? NameLower => Name?.ToLowerInvariant(); +} diff --git a/src/Sonarr.Api.V5/Localization/LocalizationController.cs b/src/Sonarr.Api.V5/Localization/LocalizationController.cs index 6e5c91eec..2f924efa8 100644 --- a/src/Sonarr.Api.V5/Localization/LocalizationController.cs +++ b/src/Sonarr.Api.V5/Localization/LocalizationController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Localization; using Sonarr.Http; @@ -17,25 +19,25 @@ public LocalizationController(ILocalizationService localizationService) protected override LocalizationResource GetResourceById(int id) { - return GetLocalization(); + return _localizationService.GetLocalizationDictionary().ToResource(); } [HttpGet] [Produces("application/json")] - public LocalizationResource GetLocalization() + public Ok<LocalizationResource> GetLocalization() { - return _localizationService.GetLocalizationDictionary().ToResource(); + return TypedResults.Ok(GetResourceById(1)); } [HttpGet("language")] [Produces("application/json")] - public LocalizationLanguageResource GetLanguage() + public Ok<LocalizationLanguageResource> GetLanguage() { var identifier = _localizationService.GetLanguageIdentifier(); - return new LocalizationLanguageResource + return TypedResults.Ok(new LocalizationLanguageResource { Identifier = identifier - }; + }); } } diff --git a/src/Sonarr.Api.V5/Logs/LogController.cs b/src/Sonarr.Api.V5/Logs/LogController.cs index 4f9a8e09a..757aec073 100644 --- a/src/Sonarr.Api.V5/Logs/LogController.cs +++ b/src/Sonarr.Api.V5/Logs/LogController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -21,11 +23,11 @@ public LogController(ILogService logService, IConfigFileProvider configFileProvi [HttpGet] [Produces("application/json")] - public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string? level) + public Ok<PagingResource<LogResource>> GetLogs([FromQuery] PagingRequestResource paging, string? level) { if (!_configFileProvider.LogDbEnabled) { - return new PagingResource<LogResource>(); + return TypedResults.Ok(new PagingResource<LogResource>()); } var pagingResource = new PagingResource<LogResource>(paging); @@ -72,7 +74,7 @@ public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource pag response.SortKey = "time"; } - return response; + return TypedResults.Ok(response); } } } diff --git a/src/Sonarr.Api.V5/Logs/LogFileControllerBase.cs b/src/Sonarr.Api.V5/Logs/LogFileControllerBase.cs index 01fed3b7e..877a63653 100644 --- a/src/Sonarr.Api.V5/Logs/LogFileControllerBase.cs +++ b/src/Sonarr.Api.V5/Logs/LogFileControllerBase.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Disk; @@ -24,7 +26,7 @@ public LogFileControllerBase(IDiskProvider diskProvider, [HttpGet] [Produces("application/json")] - public List<LogFileResource> GetLogFilesResponse() + public Ok<List<LogFileResource>> GetLogFilesResponse() { var result = new List<LogFileResource>(); @@ -45,12 +47,12 @@ public List<LogFileResource> GetLogFilesResponse() }); } - return result.OrderByDescending(l => l.LastWriteTime).ToList(); + return TypedResults.Ok(result.OrderByDescending(l => l.LastWriteTime).ToList()); } [HttpGet(@"{filename:regex([[-.a-zA-Z0-9]]+?\.txt)}")] [Produces("text/plain")] - public IActionResult GetLogFileResponse(string filename) + public Results<PhysicalFileHttpResult, NotFound> GetLogFileResponse(string filename) { LogManager.Flush(); @@ -58,10 +60,10 @@ public IActionResult GetLogFileResponse(string filename) if (!_diskProvider.FileExists(filePath)) { - return NotFound(); + return TypedResults.NotFound(); } - return PhysicalFile(filePath, "text/plain"); + return TypedResults.PhysicalFile(filePath, "text/plain"); } protected abstract IEnumerable<string> GetLogFiles(); diff --git a/src/Sonarr.Api.V5/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V5/ManualImport/ManualImportController.cs index 2fb8d79b0..bf9ad9bec 100644 --- a/src/Sonarr.Api.V5/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V5/ManualImport/ManualImportController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; @@ -20,14 +22,14 @@ public ManualImportController(IManualImportService manualImportService) [HttpGet] [Produces("application/json")] - public List<ManualImportResource> GetMediaFiles(string? folder, [FromQuery] string[]? downloadIds, int? seriesId, int? seasonNumber, bool filterExistingFiles = true) + public Ok<List<ManualImportResource>> GetMediaFiles(string? folder, [FromQuery] string[]? downloadIds, int? seriesId, int? seasonNumber, bool filterExistingFiles = true) { if (seriesId.HasValue && downloadIds == null) { - return _manualImportService.GetMediaFiles(seriesId.Value, seasonNumber) + return TypedResults.Ok(_manualImportService.GetMediaFiles(seriesId.Value, seasonNumber) .ToResource() .Select(AddQualityWeight) - .ToList(); + .ToList()); } if (downloadIds != null && downloadIds.Any()) @@ -39,20 +41,20 @@ public List<ManualImportResource> GetMediaFiles(string? folder, [FromQuery] stri files.AddRange(_manualImportService.GetMediaFiles(null, downloadId, seriesId, filterExistingFiles)); } - return files.ToResource() + return TypedResults.Ok(files.ToResource() .Select(AddQualityWeight) - .ToList(); + .ToList()); } - return _manualImportService.GetMediaFiles(folder, null, seriesId, filterExistingFiles) + return TypedResults.Ok(_manualImportService.GetMediaFiles(folder, null, seriesId, filterExistingFiles) .ToResource() .Select(AddQualityWeight) - .ToList(); + .ToList()); } [HttpPost] [Consumes("application/json")] - public List<ManualImportResource> ReprocessItems([FromBody] List<ManualImportReprocessResource> items) + public Results<Ok<List<ManualImportResource>>, BadRequest> ReprocessItems([FromBody] List<ManualImportReprocessResource> items) { if (items is { Count: 0 }) { @@ -87,10 +89,15 @@ public List<ManualImportResource> ReprocessItems([FromBody] List<ManualImportRep processedItem.SeasonNumber = item.SeasonNumber; } + if (item.RelativePath.IsNotNullOrWhiteSpace()) + { + processedItem.RelativePath = item.RelativePath; + } + updatedItems.Add(processedItem); } - return updatedItems.ToResource(); + return TypedResults.Ok(updatedItems.ToResource()); } private ManualImportResource AddQualityWeight(ManualImportResource item) diff --git a/src/Sonarr.Api.V5/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V5/ManualImport/ManualImportReprocessResource.cs index bd5e3a587..c12896652 100644 --- a/src/Sonarr.Api.V5/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V5/ManualImport/ManualImportReprocessResource.cs @@ -10,6 +10,7 @@ namespace Sonarr.Api.V5.ManualImport; public class ManualImportReprocessResource : RestResource { public string? Path { get; set; } + public string? RelativePath { get; set; } public int SeriesId { get; set; } public int? SeasonNumber { get; set; } public List<EpisodeResource> Episodes { get; set; } = []; diff --git a/src/Sonarr.Api.V5/Metadata/MetadataController.cs b/src/Sonarr.Api.V5/Metadata/MetadataController.cs index 380324856..1435db9bb 100644 --- a/src/Sonarr.Api.V5/Metadata/MetadataController.cs +++ b/src/Sonarr.Api.V5/Metadata/MetadataController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Extras.Metadata; using NzbDrone.SignalR; @@ -18,13 +19,13 @@ public MetadataController(IBroadcastSignalRMessage signalRBroadcaster, IMetadata } [NonAction] - public override ActionResult<MetadataResource> UpdateProvider([FromBody] MetadataBulkResource providerResource) + public override Results<Ok<IEnumerable<MetadataResource>>, BadRequest> UpdateProvider([FromBody] MetadataBulkResource providerResource) { throw new NotImplementedException(); } [NonAction] - public override ActionResult DeleteProviders([FromBody] MetadataBulkResource resource) + public override NoContent DeleteProviders([FromBody] MetadataBulkResource resource) { throw new NotImplementedException(); } diff --git a/src/Sonarr.Api.V5/Parse/ParseController.cs b/src/Sonarr.Api.V5/Parse/ParseController.cs index d55d5b4d7..8ca7f7b45 100644 --- a/src/Sonarr.Api.V5/Parse/ParseController.cs +++ b/src/Sonarr.Api.V5/Parse/ParseController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; @@ -28,24 +30,24 @@ public ParseController(IParsingService parsingService, [HttpGet] [Produces("application/json")] - public ParseResource Parse(string? title, string? path) + public Ok<ParseResource> Parse(string? title, string? path) { if (title.IsNullOrWhiteSpace()) { - return new ParseResource + return TypedResults.Ok(new ParseResource { Title = title - }; + }); } var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParsePath(path) : Parser.ParseTitle(title); if (parsedEpisodeInfo == null) { - return new ParseResource + return TypedResults.Ok(new ParseResource { Title = title - }; + }); } var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0, null); @@ -57,7 +59,7 @@ public ParseResource Parse(string? title, string? path) remoteEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(remoteEpisode, 0); remoteEpisode.CustomFormatScore = remoteEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(remoteEpisode.CustomFormats) ?? 0; - return new ParseResource + return TypedResults.Ok(new ParseResource { Title = title, ParsedEpisodeInfo = remoteEpisode.ParsedEpisodeInfo, @@ -66,15 +68,15 @@ public ParseResource Parse(string? title, string? path) Languages = remoteEpisode.Languages, CustomFormats = remoteEpisode.CustomFormats?.ToResource(false), CustomFormatScore = remoteEpisode.CustomFormatScore - }; + }); } else { - return new ParseResource + return TypedResults.Ok(new ParseResource { Title = title, ParsedEpisodeInfo = parsedEpisodeInfo - }; + }); } } } diff --git a/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileController.cs index a827635ab..4bd8bbc9f 100644 --- a/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileController.cs +++ b/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; @@ -46,30 +48,30 @@ public QualityProfileController(IQualityProfileService profileService, ICustomFo [RestPostById] [Consumes("application/json")] - public ActionResult<QualityProfileResource> Create([FromBody] QualityProfileResource resource) + public Results<Created<QualityProfileResource>, NotFound> Create([FromBody] QualityProfileResource resource) { var model = resource.ToModel(); model = _profileService.Add(model); - return Created(model.Id); + return TypedCreated(model.Id); } [RestDeleteById] - public ActionResult DeleteProfile(int id) + public NoContent DeleteProfile(int id) { _profileService.Delete(id); - return NoContent(); + return TypedResults.NoContent(); } [RestPutById] [Consumes("application/json")] - public ActionResult<QualityProfileResource> Update([FromBody] QualityProfileResource resource) + public Results<Accepted<QualityProfileResource>, NotFound> Update([FromBody] QualityProfileResource resource) { var model = resource.ToModel(); _profileService.Update(model); - return Accepted(model.Id); + return TypedAccepted(model.Id); } protected override QualityProfileResource GetResourceById(int id) @@ -79,8 +81,8 @@ protected override QualityProfileResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<QualityProfileResource> GetAll() + public Ok<List<QualityProfileResource>> GetAll() { - return _profileService.All().ToResource(); + return TypedResults.Ok(_profileService.All().ToResource()); } } diff --git a/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileSchemaController.cs b/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileSchemaController.cs index ba2a0afba..838cef4ec 100644 --- a/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileSchemaController.cs +++ b/src/Sonarr.Api.V5/Profiles/Quality/QualityProfileSchemaController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Profiles.Qualities; using Sonarr.Http; @@ -15,11 +17,11 @@ public QualityProfileSchemaController(IQualityProfileService profileService) } [HttpGet] - public QualityProfileResource GetSchema() + public Ok<QualityProfileResource> GetSchema() { var qualityProfile = _profileService.GetDefaultProfile(string.Empty); - return qualityProfile.ToResource(); + return TypedResults.Ok(qualityProfile.ToResource()); } } } diff --git a/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileController.cs b/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileController.cs index cdbd144a1..88088a0e5 100644 --- a/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileController.cs +++ b/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Indexers; @@ -21,7 +23,7 @@ public ReleaseProfileController(IReleaseProfileService releaseProfileService, II SharedValidator.RuleFor(d => d).Custom((restriction, context) => { - if (restriction.Required.Empty() && restriction.Ignored.Empty()) + if (restriction.Required.Empty() && restriction.Ignored.Empty() && !restriction.AirDateRestriction) { context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required"); } @@ -52,29 +54,29 @@ public ReleaseProfileController(IReleaseProfileService releaseProfileService, II } [RestPostById] - public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource) + public Results<Created<ReleaseProfileResource>, NotFound> Create([FromBody] ReleaseProfileResource resource) { var model = resource.ToModel(); model = _releaseProfileService.Add(model); - return Created(model.Id); + return TypedCreated(model.Id); } [RestDeleteById] - public ActionResult DeleteProfile(int id) + public NoContent DeleteProfile(int id) { _releaseProfileService.Delete(id); - return NoContent(); + return TypedResults.NoContent(); } [RestPutById] - public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileResource resource) + public Results<Accepted<ReleaseProfileResource>, NotFound> Update([FromBody] ReleaseProfileResource resource) { var model = resource.ToModel(); _releaseProfileService.Update(model); - return Accepted(model.Id); + return TypedAccepted(model.Id); } protected override ReleaseProfileResource GetResourceById(int id) @@ -83,8 +85,8 @@ protected override ReleaseProfileResource GetResourceById(int id) } [HttpGet] - public List<ReleaseProfileResource> GetAll() + public Ok<List<ReleaseProfileResource>> GetAll() { - return _releaseProfileService.All().ToResource(); + return TypedResults.Ok(_releaseProfileService.All().ToResource()); } } diff --git a/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileResource.cs b/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileResource.cs index 2a1448b8a..a04101137 100644 --- a/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileResource.cs +++ b/src/Sonarr.Api.V5/Profiles/Release/ReleaseProfileResource.cs @@ -9,6 +9,8 @@ public class ReleaseProfileResource : RestResource public bool Enabled { get; set; } public List<string> Required { get; set; } = []; public List<string> Ignored { get; set; } = []; + public bool AirDateRestriction { get; set; } + public int AirDateGracePeriod { get; set; } public List<int> IndexerIds { get; set; } = []; public HashSet<int> Tags { get; set; } = []; public HashSet<int> ExcludedTags { get; set; } = []; @@ -25,6 +27,8 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model) Enabled = model.Enabled, Required = model.Required ?? [], Ignored = model.Ignored ?? [], + AirDateRestriction = model.AirDateRestriction, + AirDateGracePeriod = model.AirDateGracePeriod, IndexerIds = model.IndexerIds ?? [], Tags = model.Tags ?? [], ExcludedTags = model.ExcludedTags ?? [], @@ -40,6 +44,8 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource) Enabled = resource.Enabled, Required = resource.Required, Ignored = resource.Ignored, + AirDateRestriction = resource.AirDateRestriction, + AirDateGracePeriod = resource.AirDateGracePeriod, IndexerIds = resource.IndexerIds, Tags = resource.Tags, ExcludedTags = resource.ExcludedTags diff --git a/src/Sonarr.Api.V5/Provider/ProviderControllerBase.cs b/src/Sonarr.Api.V5/Provider/ProviderControllerBase.cs index 6b5ceb3d0..ff95083f9 100644 --- a/src/Sonarr.Api.V5/Provider/ProviderControllerBase.cs +++ b/src/Sonarr.Api.V5/Provider/ProviderControllerBase.cs @@ -1,5 +1,7 @@ using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; @@ -57,7 +59,7 @@ protected override TProviderResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<TProviderResource> GetAll() + public Ok<List<TProviderResource>> GetAll() { var providerDefinitions = _providerFactory.All(); @@ -70,48 +72,48 @@ public List<TProviderResource> GetAll() result.Add(_resourceMapper.ToResource(definition)); } - return result.OrderBy(p => p.Name).ToList(); + return TypedResults.Ok(result.OrderBy(p => p.Name).ToList()); } [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<TProviderResource> CreateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) + public Results<Created<TProviderResource>, NotFound> CreateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool skipTesting = false, [FromQuery] SkipValidation skipValidation = SkipValidation.None) { - var providerDefinition = GetDefinition(providerResource, null, true, !forceSave, false); + var providerDefinition = GetDefinition(providerResource, null, skipValidation, false); - if (providerDefinition.Enable) + if (providerDefinition.Enable && !skipTesting) { - Test(providerDefinition, !forceSave); + Test(providerDefinition, skipValidation); } providerDefinition = _providerFactory.Create(providerDefinition); - return Created(providerDefinition.Id); + return TypedCreated(providerDefinition.Id); } [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<TProviderResource> UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) + public Results<Accepted<TProviderResource>, NotFound> UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool skipTesting = false, [FromQuery] SkipValidation skipValidation = SkipValidation.None) { // 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(); + return TypedResults.NotFound(); } - var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false); + var providerDefinition = GetDefinition(providerResource, existingDefinition, skipValidation, false); // Compare settings separately because they are not serialized with the definition. 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) + // Only test existing definitions if it is enabled, skipTesting isn't set and the definition has changed. + if (providerDefinition.Enable && !skipTesting && hasDefinitionChanged) { - Test(providerDefinition, true); + Test(providerDefinition, skipValidation); } if (hasDefinitionChanged) @@ -119,13 +121,13 @@ public ActionResult<TProviderResource> UpdateProvider([FromRoute] int id, [FromB _providerFactory.Update(providerDefinition); } - return Accepted(existingDefinition.Id); + return TypedAccepted(existingDefinition.Id); } [HttpPut("bulk")] [Consumes("application/json")] [Produces("application/json")] - public virtual ActionResult<TProviderResource> UpdateProvider([FromBody] TBulkProviderResource providerResource) + public virtual Results<Ok<IEnumerable<TProviderResource>>, BadRequest> UpdateProvider([FromBody] TBulkProviderResource providerResource) { if (!providerResource.Ids.Any()) { @@ -160,41 +162,41 @@ public virtual ActionResult<TProviderResource> UpdateProvider([FromBody] TBulkPr _bulkResourceMapper.UpdateModel(providerResource, definitionsToUpdate); - return Accepted(_providerFactory.Update(definitionsToUpdate).Select(x => _resourceMapper.ToResource(x))); + return TypedResults.Ok(_providerFactory.Update(definitionsToUpdate).Select(x => _resourceMapper.ToResource(x))); } - private TProviderDefinition GetDefinition(TProviderResource providerResource, TProviderDefinition? existingDefinition, bool validate, bool includeWarnings, bool forceValidate) + private TProviderDefinition GetDefinition(TProviderResource providerResource, TProviderDefinition? existingDefinition, SkipValidation skipValidation, bool forceValidate) { var definition = _resourceMapper.ToModel(providerResource, existingDefinition); - if (validate && (definition.Enable || forceValidate)) + if (skipValidation != SkipValidation.All && (definition.Enable || forceValidate)) { - Validate(definition, includeWarnings); + Validate(definition, skipValidation); } return definition; } [RestDeleteById] - public ActionResult DeleteProvider(int id) + public NoContent DeleteProvider(int id) { _providerFactory.Delete(id); - return NoContent(); + return TypedResults.NoContent(); } [HttpDelete("bulk")] [Consumes("application/json")] - public virtual ActionResult DeleteProviders([FromBody] TBulkProviderResource resource) + public virtual NoContent DeleteProviders([FromBody] TBulkProviderResource resource) { _providerFactory.Delete(resource.Ids); - return NoContent(); + return TypedResults.NoContent(); } [HttpGet("schema")] [Produces("application/json")] - public List<TProviderResource> GetTemplates() + public Ok<List<TProviderResource>> GetTemplates() { var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); @@ -212,25 +214,25 @@ public List<TProviderResource> GetTemplates() result.Add(providerResource); } - return result; + return TypedResults.Ok(result); } [SkipValidation(true, false)] [HttpPost("test")] [Consumes("application/json")] - public ActionResult Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false) + public NoContent Test([FromBody] TProviderResource providerResource, [FromQuery] SkipValidation skipValidation = SkipValidation.None) { var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null; - var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true); + var providerDefinition = GetDefinition(providerResource, existingDefinition, skipValidation, true); - Test(providerDefinition, true); + Test(providerDefinition, skipValidation); - return NoContent(); + return TypedResults.NoContent(); } [HttpPost("testall")] [Produces("application/json")] - public IActionResult TestAll() + public Results<Ok<List<ProviderTestAllResult>>, BadRequest<List<ProviderTestAllResult>>> TestAll() { var providerDefinitions = _providerFactory.All() .Where(c => c.Settings.Validate().IsValid && c.Enable) @@ -251,23 +253,23 @@ public IActionResult TestAll() }); } - return result.Any(c => !c.IsValid) ? BadRequest(result) : Ok(result); + return result.Any(c => !c.IsValid) ? TypedResults.BadRequest(result) : TypedResults.Ok(result); } [SkipValidation] [HttpPost("action/{name}")] [Consumes("application/json")] [Produces("application/json")] - public IActionResult RequestAction([FromRoute] string name, [FromBody] TProviderResource providerResource) + public Results<ContentHttpResult, BadRequest> 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); + var providerDefinition = GetDefinition(providerResource, existingDefinition, SkipValidation.All, false); var query = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()); var data = _providerFactory.RequestAction(providerDefinition, name, query); - return Content(data.ToJson(), "application/json"); + return TypedResults.Content(data.ToJson(), "application/json"); } [NonAction] @@ -288,30 +290,30 @@ public virtual void Handle(ProviderDeletedEvent<TProvider> message) BroadcastResourceChange(ModelAction.Deleted, message.ProviderId); } - private void Validate(TProviderDefinition definition, bool includeWarnings) + private void Validate(TProviderDefinition definition, SkipValidation skipValidation) { var validationResult = definition.Settings.Validate(); - VerifyValidationResult(validationResult, includeWarnings); + VerifyValidationResult(validationResult, skipValidation); } - protected virtual void Test(TProviderDefinition definition, bool includeWarnings) + protected virtual void Test(TProviderDefinition definition, SkipValidation skipValidation) { var validationResult = _providerFactory.Test(definition); - VerifyValidationResult(validationResult, includeWarnings); + VerifyValidationResult(validationResult, skipValidation); } - protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) + protected void VerifyValidationResult(ValidationResult validationResult, SkipValidation skipValidation) { var result = validationResult as NzbDroneValidationResult ?? new NzbDroneValidationResult(validationResult.Errors); - if (includeWarnings && (!result.IsValid || result.HasWarnings)) + if (skipValidation == SkipValidation.None && (!result.IsValid || result.HasWarnings)) { throw new ValidationException(result.Failures); } - if (!result.IsValid) + if (skipValidation == SkipValidation.Warnings && !result.IsValid) { throw new ValidationException(result.Errors); } diff --git a/src/Sonarr.Api.V5/Provider/SkipValidation.cs b/src/Sonarr.Api.V5/Provider/SkipValidation.cs new file mode 100644 index 000000000..93b4a2b93 --- /dev/null +++ b/src/Sonarr.Api.V5/Provider/SkipValidation.cs @@ -0,0 +1,9 @@ +namespace Sonarr.Api.V5.Provider +{ + public enum SkipValidation + { + None = 0, + Warnings = 1, + All = 2 + } +} diff --git a/src/Sonarr.Api.V5/Qualities/QualityDefinitionController.cs b/src/Sonarr.Api.V5/Qualities/QualityDefinitionController.cs index ff0bc820f..bf52770f7 100644 --- a/src/Sonarr.Api.V5/Qualities/QualityDefinitionController.cs +++ b/src/Sonarr.Api.V5/Qualities/QualityDefinitionController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; @@ -29,7 +31,7 @@ public QualityDefinitionController( } [RestPutById] - public ActionResult<QualityDefinitionResource> Update([FromBody] QualityDefinitionResource resource) + public Results<Accepted<QualityDefinitionResource>, NotFound> Update([FromBody] QualityDefinitionResource resource) { var model = resource.ToModel(); _qualityDefinitionService.Update(model); @@ -39,7 +41,7 @@ public ActionResult<QualityDefinitionResource> Update([FromBody] QualityDefiniti _qualityProfileService.UpdateAllSizeLimits(new QualityProfileSizeLimit(model)); } - return Accepted(model.Id); + return TypedAccepted(model.Id); } protected override QualityDefinitionResource GetResourceById(int id) @@ -48,13 +50,13 @@ protected override QualityDefinitionResource GetResourceById(int id) } [HttpGet] - public List<QualityDefinitionResource> GetAll() + public Ok<List<QualityDefinitionResource>> GetAll() { - return _qualityDefinitionService.All().ToResource(); + return TypedResults.Ok(_qualityDefinitionService.All().ToResource()); } [HttpPut] - public object UpdateMany([FromBody] List<QualityDefinitionResource> resource) + public Ok<List<QualityDefinitionResource>> UpdateMany([FromBody] List<QualityDefinitionResource> resource) { // Read from request var qualityDefinitions = resource.ToModel().ToList(); @@ -71,8 +73,7 @@ public object UpdateMany([FromBody] List<QualityDefinitionResource> resource) _qualityProfileService.UpdateAllSizeLimits(toUpdate); } - return Accepted(_qualityDefinitionService.All() - .ToResource()); + return TypedResults.Ok(_qualityDefinitionService.All().ToResource()); } [NonAction] diff --git a/src/Sonarr.Api.V5/Queue/QueueActionController.cs b/src/Sonarr.Api.V5/Queue/QueueActionController.cs index 959cebc7a..3b4c97d86 100644 --- a/src/Sonarr.Api.V5/Queue/QueueActionController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueActionController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; @@ -20,7 +22,7 @@ public QueueActionController(IPendingReleaseService pendingReleaseService, } [HttpPost("grab/{id:int}")] - public async Task<object> Grab([FromRoute] int id) + public async Task<NoContent> Grab([FromRoute] int id) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -31,12 +33,12 @@ public async Task<object> Grab([FromRoute] int id) await _downloadService.DownloadReport(pendingRelease.RemoteEpisode, null); - return new { }; + return TypedResults.NoContent(); } [HttpPost("grab/bulk")] [Consumes("application/json")] - public async Task<object> Grab([FromBody] QueueBulkResource resource) + public async Task<NoContent> Grab([FromBody] QueueBulkResource resource) { foreach (var id in resource.Ids) { @@ -50,7 +52,7 @@ public async Task<object> Grab([FromBody] QueueBulkResource resource) await _downloadService.DownloadReport(pendingRelease.RemoteEpisode, null); } - return new { }; + return TypedResults.NoContent(); } } } diff --git a/src/Sonarr.Api.V5/Queue/QueueController.cs b/src/Sonarr.Api.V5/Queue/QueueController.cs index 4fe384345..3ad9418be 100644 --- a/src/Sonarr.Api.V5/Queue/QueueController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Blocklisting; @@ -57,7 +59,7 @@ public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage, } [NonAction] - public override ActionResult<QueueResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } @@ -68,7 +70,7 @@ protected override QueueResource GetResourceById(int id) } [RestDeleteById] - public ActionResult RemoveAction(int id, string? message = null, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false) + public Results<NoContent, NotFound> RemoveAction(int id, string? message = null, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false) { var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); @@ -76,7 +78,7 @@ public ActionResult RemoveAction(int id, string? message = null, bool removeFrom { Remove(pendingRelease, message, blocklist); - return NoContent(); + return TypedResults.NoContent(); } var trackedDownload = GetTrackedDownload(id); @@ -89,11 +91,11 @@ public ActionResult RemoveAction(int id, string? message = null, bool removeFrom Remove(trackedDownload, message, removeFromClient, blocklist, skipRedownload, changeCategory); _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); - return NoContent(); + return TypedResults.NoContent(); } [HttpDelete("bulk")] - public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] string? message, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false) + public NoContent RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] string? message, [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>(); @@ -130,12 +132,12 @@ public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] stri _trackedDownloadService.StopTracking(trackedDownloadIds); - return NoContent(); + return TypedResults.NoContent(); } [HttpGet] [Produces("application/json")] - public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = true, [FromQuery] int[]? seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] QueueStatus[]? status = null, [FromQuery] QueueSubresource[]? includeSubresources = null) + public Ok<PagingResource<QueueResource>> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = true, [FromQuery] int[]? seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[]? languages = null, [FromQuery] int[]? quality = null, [FromQuery] QueueStatus[]? status = null, [FromQuery] QueueSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource<QueueResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>( @@ -167,7 +169,7 @@ public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource var includeSeries = includeSubresources.Contains(QueueSubresource.Series); var includeEpisodes = includeSubresources.Contains(QueueSubresource.Episodes); - return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet() ?? [], protocol, languages?.ToHashSet() ?? [], quality?.ToHashSet() ?? [], status?.ToHashSet() ?? [], includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisodes)); + return TypedResults.Ok(pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet() ?? [], protocol, languages?.ToHashSet() ?? [], quality?.ToHashSet() ?? [], status?.ToHashSet() ?? [], includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisodes))); } 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) diff --git a/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs b/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs index c6d5526a9..c4ebdd73d 100644 --- a/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueDetailsController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; @@ -25,7 +27,7 @@ public QueueDetailsController(IBroadcastSignalRMessage broadcastSignalRMessage, } [NonAction] - public override ActionResult<QueueResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } @@ -37,7 +39,7 @@ protected override QueueResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<QueueResource> GetQueue(int? seriesId, [FromQuery]List<int> episodeIds, [FromQuery] QueueSubresource[]? includeSubresources = null) + public Ok<List<QueueResource>> GetQueue(int? seriesId, [FromQuery]List<int> episodeIds, [FromQuery] QueueSubresource[]? includeSubresources = null) { var queue = _queueService.GetQueue(); var pending = _pendingReleaseService.GetPendingQueue(); @@ -47,17 +49,17 @@ public List<QueueResource> GetQueue(int? seriesId, [FromQuery]List<int> episodeI if (seriesId.HasValue) { - return fullQueue.Where(q => q.Series?.Id == seriesId).ToResource(includeSeries, includeEpisodes); + return TypedResults.Ok(fullQueue.Where(q => q.Series?.Id == seriesId).ToResource(includeSeries, includeEpisodes)); } if (episodeIds.Any()) { - return fullQueue.Where(q => q.Episodes.Any() && + return TypedResults.Ok(fullQueue.Where(q => q.Episodes.Any() && episodeIds.IntersectBy(e => e, q.Episodes, e => e.Id, null).Any()) - .ToResource(includeSeries, includeEpisodes); + .ToResource(includeSeries, includeEpisodes)); } - return fullQueue.ToResource(includeSeries, includeEpisodes); + return TypedResults.Ok(fullQueue.ToResource(includeSeries, includeEpisodes)); } [NonAction] diff --git a/src/Sonarr.Api.V5/Queue/QueueStatusController.cs b/src/Sonarr.Api.V5/Queue/QueueStatusController.cs index ff57c76bb..7ba949bbe 100644 --- a/src/Sonarr.Api.V5/Queue/QueueStatusController.cs +++ b/src/Sonarr.Api.V5/Queue/QueueStatusController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; @@ -29,7 +30,7 @@ public QueueStatusController(IBroadcastSignalRMessage broadcastSignalRMessage, I } [NonAction] - public override ActionResult<QueueStatusResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<QueueStatusResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } diff --git a/src/Sonarr.Api.V5/Release/ReleaseController.cs b/src/Sonarr.Api.V5/Release/ReleaseController.cs index 66a6b1ac5..968684419 100644 --- a/src/Sonarr.Api.V5/Release/ReleaseController.cs +++ b/src/Sonarr.Api.V5/Release/ReleaseController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Cache; @@ -71,7 +73,7 @@ public ReleaseController(IFetchAndParseRss rssFetcherAndParser, } [NonAction] - public override ActionResult<ReleaseResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<ReleaseResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } @@ -83,7 +85,7 @@ protected override ReleaseResource GetResourceById(int id) [HttpPost] [Consumes("application/json")] - public async Task<object> DownloadRelease([FromBody] ReleaseGrabResource release) + public async Task<Results<Ok<ReleaseGrabResource>, NotFound>> DownloadRelease([FromBody] ReleaseGrabResource release) { var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release)); @@ -182,24 +184,24 @@ public async Task<object> DownloadRelease([FromBody] ReleaseGrabResource release throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); } - return release; + return TypedResults.Ok(release); } [HttpGet] [Produces("application/json")] - public async Task<List<ReleaseResource>> GetReleases(int? seriesId, int? episodeId, int? seasonNumber) + public async Task<Results<Ok<List<ReleaseResource>>, BadRequest>> GetReleases(int? seriesId, int? episodeId, int? seasonNumber) { if (episodeId.HasValue) { - return await GetEpisodeReleases(episodeId.Value); + return TypedResults.Ok(await GetEpisodeReleases(episodeId.Value)); } if (seriesId.HasValue && seasonNumber.HasValue) { - return await GetSeasonReleases(seriesId.Value, seasonNumber.Value); + return TypedResults.Ok(await GetSeasonReleases(seriesId.Value, seasonNumber.Value)); } - return await GetRss(); + return TypedResults.Ok(await GetRss()); } private async Task<List<ReleaseResource>> GetEpisodeReleases(int episodeId) diff --git a/src/Sonarr.Api.V5/Release/ReleasePushController.cs b/src/Sonarr.Api.V5/Release/ReleasePushController.cs index a23e99422..8363f2d92 100644 --- a/src/Sonarr.Api.V5/Release/ReleasePushController.cs +++ b/src/Sonarr.Api.V5/Release/ReleasePushController.cs @@ -1,5 +1,7 @@ using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NLog; using NzbDrone.Common.Extensions; @@ -50,7 +52,7 @@ public ReleasePushController(IMakeDownloadDecision downloadDecisionMaker, [HttpPost] [Consumes("application/json")] - public ActionResult<ReleaseResource> Create([FromBody] ReleasePushResource release) + public Results<Ok<ReleaseResource>, BadRequest> Create([FromBody] ReleasePushResource release) { _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl); @@ -80,7 +82,7 @@ public ActionResult<ReleaseResource> Create([FromBody] ReleasePushResource relea throw new ValidationException(new List<ValidationFailure> { new("Title", "Unable to parse", release.Title) }); } - return decision.MapDecision(1, _qualityProfile); + return TypedResults.Ok(decision.MapDecision(1, _qualityProfile)); } private void ResolveIndexer(ReleaseInfo release) diff --git a/src/Sonarr.Api.V5/RemotePathMappings/RemotePathMappingController.cs b/src/Sonarr.Api.V5/RemotePathMappings/RemotePathMappingController.cs index ca7defaf1..36f167e61 100644 --- a/src/Sonarr.Api.V5/RemotePathMappings/RemotePathMappingController.cs +++ b/src/Sonarr.Api.V5/RemotePathMappings/RemotePathMappingController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; @@ -47,33 +49,33 @@ protected override RemotePathMappingResource GetResourceById(int id) [RestPostById] [Consumes("application/json")] - public ActionResult<RemotePathMappingResource> CreateMapping([FromBody] RemotePathMappingResource resource) + public Results<Created<RemotePathMappingResource>, NotFound> CreateMapping([FromBody] RemotePathMappingResource resource) { var model = resource.ToModel(); - return Created(_remotePathMappingService.Add(model).Id); + return TypedCreated(_remotePathMappingService.Add(model).Id); } [HttpGet] [Produces("application/json")] - public List<RemotePathMappingResource> GetMappings() + public Ok<List<RemotePathMappingResource>> GetMappings() { - return _remotePathMappingService.All().ToResource(); + return TypedResults.Ok(_remotePathMappingService.All().ToResource()); } [RestDeleteById] - public ActionResult DeleteMapping(int id) + public NoContent DeleteMapping(int id) { _remotePathMappingService.Remove(id); - return NoContent(); + return TypedResults.NoContent(); } [RestPutById] - public ActionResult<RemotePathMappingResource> UpdateMapping([FromBody] RemotePathMappingResource resource) + public Results<Ok<RemotePathMappingResource>, NotFound> UpdateMapping([FromBody] RemotePathMappingResource resource) { var mapping = resource.ToModel(); - return Accepted(_remotePathMappingService.Update(mapping)); + return TypedResults.Ok(_remotePathMappingService.Update(mapping).ToResource()); } } diff --git a/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs b/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs index 19b9a3aec..3f650a693 100644 --- a/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs +++ b/src/Sonarr.Api.V5/RootFolders/RootFolderController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation.Paths; @@ -49,25 +51,25 @@ protected override RootFolderResource GetResourceById(int id) [RestPostById] [Consumes("application/json")] - public ActionResult<RootFolderResource> CreateRootFolder([FromBody] RootFolderResource rootFolderResource) + public Results<Created<RootFolderResource>, NotFound> CreateRootFolder([FromBody] RootFolderResource rootFolderResource) { var model = rootFolderResource.ToModel(); - return Created(_rootFolderService.Add(model).Id); + return TypedCreated(_rootFolderService.Add(model).Id); } [HttpGet] [Produces("application/json")] - public List<RootFolderResource> GetRootFolders() + public Ok<List<RootFolderResource>> GetRootFolders() { - return _rootFolderService.AllWithUnmappedFolders().ToResource(); + return TypedResults.Ok(_rootFolderService.AllWithUnmappedFolders().ToResource()); } [RestDeleteById] - public ActionResult DeleteFolder(int id) + public NoContent DeleteFolder(int id) { _rootFolderService.Remove(id); - return NoContent(); + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/SeasonPass/SeasonPassController.cs b/src/Sonarr.Api.V5/SeasonPass/SeasonPassController.cs index dc55e7d74..28ddcd5c7 100644 --- a/src/Sonarr.Api.V5/SeasonPass/SeasonPassController.cs +++ b/src/Sonarr.Api.V5/SeasonPass/SeasonPassController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tv; using Sonarr.Http; @@ -18,7 +20,7 @@ public SeasonPassController(ISeriesService seriesService, IEpisodeMonitoredServi [HttpPost] [Consumes("application/json")] - public IActionResult UpdateAll([FromBody] SeasonPassResource resource) + public NoContent UpdateAll([FromBody] SeasonPassResource resource) { var seriesToUpdate = _seriesService.GetSeries(resource.Series.Select(s => s.Id)); @@ -52,6 +54,6 @@ public IActionResult UpdateAll([FromBody] SeasonPassResource resource) _episodeMonitoredService.SetEpisodeMonitoredStatus(series, resource.MonitoringOptions.ToModel()); } - return NoContent(); + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs b/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs index c338cfbcb..1387147a8 100644 --- a/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs +++ b/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs @@ -1,3 +1,5 @@ +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.SeriesStats; namespace Sonarr.Api.V5.Series; @@ -12,6 +14,8 @@ public class SeasonStatisticsResource public int MonitoredEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public List<string>? ReleaseGroups { get; set; } + public List<ReleaseType>? ReleaseTypes { get; set; } + public List<Quality>? EpisodeFileQualities { get; set; } public decimal PercentOfEpisodes { @@ -40,7 +44,9 @@ public static SeasonStatisticsResource ToResource(this SeasonStatistics model) TotalEpisodeCount = model.TotalEpisodeCount, MonitoredEpisodeCount = model.MonitoredEpisodeCount, SizeOnDisk = model.SizeOnDisk, - ReleaseGroups = model.ReleaseGroups + ReleaseGroups = model.ReleaseGroups, + ReleaseTypes = model.ReleaseTypes, + EpisodeFileQualities = model.EpisodeFileQualities }; } } diff --git a/src/Sonarr.Api.V5/Series/SeriesController.cs b/src/Sonarr.Api.V5/Series/SeriesController.cs index 992d4c99d..5eb93f035 100644 --- a/src/Sonarr.Api.V5/Series/SeriesController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Common.TPL; @@ -107,7 +109,7 @@ public SeriesController(IBroadcastSignalRMessage signalRBroadcaster, [HttpGet] [Produces("application/json")] - public List<SeriesResource> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null) + public Ok<List<SeriesResource>> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource[]? includeSubresources = null) { var seriesStats = _seriesStatisticsService.SeriesStatistics(); var seriesResources = new List<SeriesResource>(); @@ -115,7 +117,7 @@ public List<SeriesResource> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource if (tvdbId.HasValue) { - seriesResources.AddIfNotNull(_seriesService.FindByTvdbId(tvdbId.Value).ToResource(includeSeasonImages)); + seriesResources.AddIfNotNull(_seriesService.FindByTvdbId(tvdbId.Value)?.ToResource(includeSeasonImages)); } else { @@ -127,18 +129,18 @@ public List<SeriesResource> AllSeries(int? tvdbId, [FromQuery] SeriesSubresource PopulateAlternateTitles(seriesResources); seriesResources.ForEach(LinkRootFolderPath); - return seriesResources; + return TypedResults.Ok(seriesResources); } [NonAction] - public override ActionResult<SeriesResource> GetResourceByIdWithErrorHandler(int id) + public override Results<Ok<SeriesResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { return base.GetResourceByIdWithErrorHandler(id); } [RestGetById] [Produces("application/json")] - public ActionResult<SeriesResource> GetResourceByIdWithErrorHandler(int id, [FromQuery] SeriesSubresource[]? includeSubresources = null) + public Results<Ok<SeriesResource>, NotFound> GetResourceByIdWithErrorHandler(int id, [FromQuery] SeriesSubresource[]? includeSubresources = null) { var includeSeasonImages = includeSubresources.Contains(SeriesSubresource.SeasonImages); @@ -146,11 +148,11 @@ public ActionResult<SeriesResource> GetResourceByIdWithErrorHandler(int id, [Fro { var series = GetSeriesResourceById(id, includeSeasonImages); - return series == null ? NotFound() : series; + return series == null ? TypedResults.NotFound() : TypedResults.Ok(series); } catch (ModelNotFoundException) { - return NotFound(); + return TypedResults.NotFound(); } } @@ -181,17 +183,17 @@ public ActionResult<SeriesResource> GetResourceByIdWithErrorHandler(int id, [Fro [RestPostById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<SeriesResource> AddSeries([FromBody] SeriesResource seriesResource) + public Results<Created<SeriesResource>, NotFound> AddSeries([FromBody] SeriesResource seriesResource) { var series = _addSeriesService.AddSeries(seriesResource.ToModel()); - return Created(series.Id); + return TypedCreated(series.Id); } [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false) + public Results<Accepted<SeriesResource>, NotFound> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false) { var series = _seriesService.GetSeries(seriesResource.Id); @@ -215,13 +217,13 @@ public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource serie BroadcastResourceChange(ModelAction.Updated, seriesResource); - return Accepted(seriesResource.Id); + return TypedAccepted(seriesResource.Id); } [HttpPut("{id}/season")] [Consumes("application/json")] [Produces("application/json")] - public ActionResult<SeasonResource> UpdateSeasonMonitored([FromRoute] int id, [FromBody] SeasonResource seasonResource) + public Results<Ok<SeasonResource>, NotFound> UpdateSeasonMonitored([FromRoute] int id, [FromBody] SeasonResource seasonResource) { lock (_seriesLockPool.GetLock(id)) { @@ -230,25 +232,25 @@ public ActionResult<SeasonResource> UpdateSeasonMonitored([FromRoute] int id, [F if (season == null) { - return NotFound(); + return TypedResults.NotFound(); } season.Monitored = seasonResource.Monitored; _seriesService.UpdateSeries(series); - BroadcastResourceChange(ModelAction.Updated, series.ToResource()); + BroadcastResourceChange(ModelAction.Updated, GetSeriesResource(series, false)!); - return season.ToResource(); + return TypedResults.Ok(season.ToResource()); } } [RestDeleteById] - public ActionResult DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false) + public NoContent DeleteSeries(int id, bool deleteFiles = false, bool addImportListExclusion = false) { _seriesService.DeleteSeries(new List<int> { id }, deleteFiles, addImportListExclusion); - return NoContent(); + return TypedResults.NoContent(); } private SeriesResource? GetSeriesResource(NzbDrone.Core.Tv.Series? series, bool includeSeasonImages) @@ -277,7 +279,7 @@ private void MapCoversToLocal(params SeriesResource[] series) private void FetchAndLinkSeriesStatistics(SeriesResource resource) { - LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id)); + LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id, resource.QualityProfileId)); } private void LinkSeriesStatistics(List<SeriesResource> resources, Dictionary<int, SeriesStatistics> seriesStatistics) diff --git a/src/Sonarr.Api.V5/Series/SeriesEditorController.cs b/src/Sonarr.Api.V5/Series/SeriesEditorController.cs index 9ce021a3c..dd3430b4b 100644 --- a/src/Sonarr.Api.V5/Series/SeriesEditorController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesEditorController.cs @@ -1,4 +1,6 @@ using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Commands; @@ -23,7 +25,7 @@ public SeriesEditorController(ISeriesService seriesService, IManageCommandQueue } [HttpPut] - public object SaveAll([FromBody] SeriesEditorResource resource) + public Results<Ok<List<SeriesResource>>, BadRequest> SaveAll([FromBody] SeriesEditorResource resource) { var seriesToUpdate = _seriesService.GetSeries(resource.SeriesIds); var seriesToMove = new List<BulkMoveSeries>(); @@ -101,14 +103,14 @@ public object SaveAll([FromBody] SeriesEditorResource resource) }); } - return Accepted(_seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles).ToResource()); + return TypedResults.Ok(_seriesService.UpdateSeries(seriesToUpdate, !resource.MoveFiles).ToResource()); } [HttpDelete] - public object DeleteSeries([FromBody] SeriesEditorResource resource) + public NoContent DeleteSeries([FromBody] SeriesEditorResource resource) { _seriesService.DeleteSeries(resource.SeriesIds, resource.DeleteFiles, resource.AddImportListExclusion); - return new { }; + return TypedResults.NoContent(); } } diff --git a/src/Sonarr.Api.V5/Series/SeriesFolderController.cs b/src/Sonarr.Api.V5/Series/SeriesFolderController.cs index b26d71286..fd1445d3b 100644 --- a/src/Sonarr.Api.V5/Series/SeriesFolderController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesFolderController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Organizer; using NzbDrone.Core.Tv; @@ -19,14 +21,14 @@ public SeriesFolderController(ISeriesService seriesService, IBuildFileNames file [HttpGet("{id}/folder")] [Produces("application/json")] - public object GetFolder([FromRoute] int id) + public Ok<object> GetFolder([FromRoute] int id) { var series = _seriesService.GetSeries(id); var folder = _fileNameBuilder.GetSeriesFolder(series); - return new + return TypedResults.Ok((object)new { folder - }; + }); } } diff --git a/src/Sonarr.Api.V5/Series/SeriesImportController.cs b/src/Sonarr.Api.V5/Series/SeriesImportController.cs index dd8d6b62a..dd5543290 100644 --- a/src/Sonarr.Api.V5/Series/SeriesImportController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesImportController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tv; using Sonarr.Http; @@ -15,11 +17,11 @@ public SeriesImportController(IAddSeriesService addSeriesService) } [HttpPost] - public object Import([FromBody] List<SeriesResource> resource) + public Ok<List<SeriesResource>> Import([FromBody] List<SeriesResource> resource) { var newSeries = resource.ToModel(); - return _addSeriesService.AddSeries(newSeries).ToResource(); + return TypedResults.Ok(_addSeriesService.AddSeries(newSeries).ToResource()); } } } diff --git a/src/Sonarr.Api.V5/Series/SeriesLookupController.cs b/src/Sonarr.Api.V5/Series/SeriesLookupController.cs index d504cca1f..f5a697109 100644 --- a/src/Sonarr.Api.V5/Series/SeriesLookupController.cs +++ b/src/Sonarr.Api.V5/Series/SeriesLookupController.cs @@ -1,4 +1,7 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Organizer; @@ -13,19 +16,21 @@ public class SeriesLookupController : Controller private readonly ISearchForNewSeries _searchProxy; private readonly IBuildFileNames _fileNameBuilder; private readonly IMapCoversToLocal _coverMapper; + private readonly IImportListExclusionService _importListExclusionService; - public SeriesLookupController(ISearchForNewSeries searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper) + public SeriesLookupController(ISearchForNewSeries searchProxy, IBuildFileNames fileNameBuilder, IMapCoversToLocal coverMapper, IImportListExclusionService importListExclusionService) { _searchProxy = searchProxy; _fileNameBuilder = fileNameBuilder; _coverMapper = coverMapper; + _importListExclusionService = importListExclusionService; } [HttpGet] - public IEnumerable<SeriesResource> Search([FromQuery] string term) + public Ok<IEnumerable<SeriesResource>> Search([FromQuery] string term) { var tvDbResults = _searchProxy.SearchForNewSeries(term); - return MapToResource(tvDbResults); + return TypedResults.Ok(MapToResource(tvDbResults)); } private IEnumerable<SeriesResource> MapToResource(IEnumerable<NzbDrone.Core.Tv.Series> series) @@ -45,6 +50,7 @@ private IEnumerable<SeriesResource> MapToResource(IEnumerable<NzbDrone.Core.Tv.S resource.Folder = _fileNameBuilder.GetSeriesFolder(currentSeries); resource.Statistics = new SeriesStatistics().ToResource(resource.Seasons); + resource.IsExcluded = _importListExclusionService.FindByTvdbId(currentSeries.TvdbId) is not null; yield return resource; } diff --git a/src/Sonarr.Api.V5/Series/SeriesResource.cs b/src/Sonarr.Api.V5/Series/SeriesResource.cs index 2d3e67e04..c955c328f 100644 --- a/src/Sonarr.Api.V5/Series/SeriesResource.cs +++ b/src/Sonarr.Api.V5/Series/SeriesResource.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; using Sonarr.Http.REST; +using Swashbuckle.AspNetCore.Annotations; namespace Sonarr.Api.V5.Series; @@ -47,12 +49,16 @@ public class SeriesResource : RestResource public string? Folder { get; set; } public string? Certification { get; set; } public List<string>? Genres { get; set; } + public string? OriginalCountry { get; set; } public HashSet<int>? Tags { get; set; } public DateTime Added { get; set; } public AddSeriesOptions? AddOptions { get; set; } public Ratings? Ratings { get; set; } public SeriesStatisticsResource? Statistics { get; set; } public bool? EpisodesChanged { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [SwaggerIgnore] + public bool? IsExcluded { get; set; } } public static class SeriesResourceMapper @@ -71,6 +77,7 @@ public static SeriesResource ToResource(this NzbDrone.Core.Tv.Series model, bool Images = model.Images.JsonClone(), Seasons = model.Seasons.ToResource(includeSeasonImages), Year = model.Year, + OriginalCountry = model.OriginalCountry, OriginalLanguage = model.OriginalLanguage, Path = model.Path, QualityProfileId = model.QualityProfileId, diff --git a/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs b/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs index d04bed7a6..ffdca151d 100644 --- a/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs +++ b/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs @@ -1,3 +1,5 @@ +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.SeriesStats; namespace Sonarr.Api.V5.Series; @@ -11,6 +13,8 @@ public class SeriesStatisticsResource public int MonitoredEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public List<string>? ReleaseGroups { get; set; } + public List<ReleaseType>? ReleaseTypes { get; set; } + public List<Quality>? EpisodeFileQualities { get; set; } public decimal PercentOfEpisodes { @@ -38,7 +42,9 @@ public static SeriesStatisticsResource ToResource(this SeriesStatistics model, L TotalEpisodeCount = model.TotalEpisodeCount, MonitoredEpisodeCount = model.MonitoredEpisodeCount, SizeOnDisk = model.SizeOnDisk, - ReleaseGroups = model.ReleaseGroups + ReleaseGroups = model.ReleaseGroups, + ReleaseTypes = model.ReleaseTypes, + EpisodeFileQualities = model.EpisodeFileQualities }; } } diff --git a/src/Sonarr.Api.V5/Settings/CertificateValidator.cs b/src/Sonarr.Api.V5/Settings/CertificateValidator.cs new file mode 100644 index 000000000..b5de3ab66 --- /dev/null +++ b/src/Sonarr.Api.V5/Settings/CertificateValidator.cs @@ -0,0 +1,75 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using FluentValidation; +using FluentValidation.Validators; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; + +namespace Sonarr.Api.V5.Settings +{ + public static class CertificateValidation + { + public static IRuleBuilderOptions<T, string> IsValidCertificate<T>(this IRuleBuilder<T, string> ruleBuilder) + { + return ruleBuilder.SetValidator(new CertificateValidator()); + } + } + + public class CertificateValidator : PropertyValidator + { + protected override string GetDefaultMessageTemplate() => "Invalid SSL certificate file or {passwordOrKey}. {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 GeneralSettingsResource resource) + { + return true; + } + + var certPath = resource.SslCertPath!; + var keyPath = resource.SslKeyPath; + var certPassword = resource.SslCertPassword; + var type = X509Certificate2.GetCertContentType(certPath); + + try + { + if (type == X509ContentType.Cert) + { + X509Certificate2.CreateFromPemFile(certPath, keyPath.IsNullOrWhiteSpace() ? null : keyPath); + } + else if (type == X509ContentType.Pkcs12) + { + X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword, X509KeyStorageFlags.DefaultKeySet); + } + else + { + Logger.Debug("Invalid SSL certificate file. Unexpected certificate type: {0}", type); + context.MessageFormatter.AppendArgument("passwordOrKey", "password"); + + return false; + } + + return true; + } + catch (CryptographicException ex) + { + var passwordOrKey = type == X509ContentType.Cert ? "key" : "password"; + + Logger.Debug(ex, "Invalid SSL certificate file or {0}. {1}", passwordOrKey, ex.Message); + + context.MessageFormatter.AppendArgument("passwordOrKey", passwordOrKey); + context.MessageFormatter.AppendArgument("message", ex.Message); + + return false; + } + } + } +} diff --git a/src/Sonarr.Api.V5/Settings/GeneralSettingsController.cs b/src/Sonarr.Api.V5/Settings/GeneralSettingsController.cs new file mode 100644 index 000000000..83e27b9b5 --- /dev/null +++ b/src/Sonarr.Api.V5/Settings/GeneralSettingsController.cs @@ -0,0 +1,114 @@ +using FluentValidation; +using Microsoft.AspNetCore.Http.HttpResults; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using Sonarr.Http; + +namespace Sonarr.Api.V5.Settings; + +[V5ApiController("settings/general")] +public class GeneralSettingsController : SettingsController<GeneralSettingsResource> +{ + private readonly IUserService _userService; + + public GeneralSettingsController(IConfigFileProvider configFileProvider, + IConfigService configService, + IUserService userService, + IDiskProvider diskProvider) + : base(configFileProvider, configService) + { + _userService = userService; + + SharedValidator.RuleFor(c => c.BindAddress) + .ValidIpAddress() + .When(c => c.BindAddress != "*" && c.BindAddress != "localhost"); + + SharedValidator.RuleFor(c => c.Port).ValidPort(); + + SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase(); + SharedValidator.RuleFor(c => c.InstanceName).StartsOrEndsWithSonarr(); + + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms); + SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod == AuthenticationType.Forms); + + SharedValidator.RuleFor(c => c.AuthenticationMethod) +#pragma warning disable CS0618 // Type or member is obsolete + .NotEqual(AuthenticationType.Basic) +#pragma warning restore CS0618 // Type or member is obsolete + .WithMessage("'Basic' is no longer supported, switch to 'Forms' instead."); + + SharedValidator.RuleFor(c => c.PasswordConfirmation) + .Must((resource, p) => IsMatchingPassword(resource)).WithMessage("Must match Password"); + + SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl); + + SharedValidator.RuleFor(c => c.SslCertPath) + .Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .SetValidator(new FileExistsValidator(diskProvider)) + .IsValidCertificate() + .When(c => c.EnableSsl); + + SharedValidator.RuleFor(c => c.SslKeyPath) + .NotEmpty() + .IsValidPath() + .SetValidator(new FileExistsValidator(diskProvider)) + .When(c => c.SslKeyPath.IsNotNullOrWhiteSpace()); + + 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); + + SharedValidator.RuleFor(c => c.BackupFolder).IsValidPath().When(c => Path.IsPathRooted(c.BackupFolder)); + SharedValidator.RuleFor(c => c.BackupInterval).InclusiveBetween(1, 7); + SharedValidator.RuleFor(c => c.BackupRetention).InclusiveBetween(1, 90); + } + + private bool IsMatchingPassword(GeneralSettingsResource resource) + { + var user = _userService.FindUser(); + + if (user != null && user.Password == resource.Password) + { + return true; + } + + if (resource.Password == resource.PasswordConfirmation) + { + return true; + } + + return false; + } + + protected override GeneralSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model) + { + var resource = GeneralSettingsResourceMapper.ToResource(configFile, model); + + var user = _userService.FindUser(); + + resource.Username = user?.Username ?? string.Empty; + resource.Password = user?.Password ?? string.Empty; + resource.PasswordConfirmation = string.Empty; + + return resource; + } + + public override Results<Accepted<GeneralSettingsResource>, NotFound> SaveSettings(GeneralSettingsResource resource) + { + if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace()) + { + _userService.Upsert(resource.Username, resource.Password); + } + + return base.SaveSettings(resource); + } +} diff --git a/src/Sonarr.Api.V5/Settings/GeneralSettingsResource.cs b/src/Sonarr.Api.V5/Settings/GeneralSettingsResource.cs new file mode 100644 index 000000000..b6c97d277 --- /dev/null +++ b/src/Sonarr.Api.V5/Settings/GeneralSettingsResource.cs @@ -0,0 +1,93 @@ +using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Security; +using NzbDrone.Core.Update; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.Settings; + +public class GeneralSettingsResource : RestResource +{ + public string? BindAddress { get; set; } + public int Port { get; set; } + public int SslPort { get; set; } + public bool EnableSsl { get; set; } + public bool LaunchBrowser { get; set; } + public AuthenticationType AuthenticationMethod { get; set; } + public AuthenticationRequiredType AuthenticationRequired { get; set; } + public bool AnalyticsEnabled { get; set; } + public string? Username { get; set; } + 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; } + public string? SslCertPath { get; set; } + public string? SslKeyPath { get; set; } + public string? SslCertPassword { get; set; } + public string? UrlBase { get; set; } + public string? InstanceName { get; set; } + public string? ApplicationUrl { get; set; } + public bool UpdateAutomatically { get; set; } + public UpdateMechanism UpdateMechanism { get; set; } + public string? UpdateScriptPath { get; set; } + public bool ProxyEnabled { get; set; } + public ProxyType ProxyType { get; set; } + public string? ProxyHostname { get; set; } + public int ProxyPort { get; set; } + public string? ProxyUsername { get; set; } + public string? ProxyPassword { get; set; } + public string? ProxyBypassFilter { get; set; } + public bool ProxyBypassLocalAddresses { get; set; } + public CertificateValidationType CertificateValidation { get; set; } + public string? BackupFolder { get; set; } + public int BackupInterval { get; set; } + public int BackupRetention { get; set; } +} + +public static class GeneralSettingsResourceMapper +{ + public static GeneralSettingsResource ToResource(IConfigFileProvider model, IConfigService configService) + { + return new GeneralSettingsResource + { + BindAddress = model.BindAddress, + Port = model.Port, + SslPort = model.SslPort, + EnableSsl = model.EnableSsl, + LaunchBrowser = model.LaunchBrowser, + AuthenticationMethod = model.AuthenticationMethod, + AuthenticationRequired = model.AuthenticationRequired, + AnalyticsEnabled = model.AnalyticsEnabled, + LogLevel = model.LogLevel, + LogSizeLimit = model.LogSizeLimit, + ConsoleLogLevel = model.ConsoleLogLevel, + Branch = model.Branch, + ApiKey = model.ApiKey, + SslCertPath = model.SslCertPath, + SslKeyPath = model.SslKeyPath, + SslCertPassword = model.SslCertPassword, + UrlBase = model.UrlBase, + InstanceName = model.InstanceName, + UpdateAutomatically = model.UpdateAutomatically, + UpdateMechanism = model.UpdateMechanism, + UpdateScriptPath = model.UpdateScriptPath, + ProxyEnabled = configService.ProxyEnabled, + ProxyType = configService.ProxyType, + ProxyHostname = configService.ProxyHostname, + ProxyPort = configService.ProxyPort, + ProxyUsername = configService.ProxyUsername, + ProxyPassword = configService.ProxyPassword, + ProxyBypassFilter = configService.ProxyBypassFilter, + ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses, + CertificateValidation = configService.CertificateValidation, + BackupFolder = configService.BackupFolder, + BackupInterval = configService.BackupInterval, + BackupRetention = configService.BackupRetention, + ApplicationUrl = configService.ApplicationUrl + }; + } +} diff --git a/src/Sonarr.Api.V5/Settings/IndexerSettingsController.cs b/src/Sonarr.Api.V5/Settings/IndexerSettingsController.cs new file mode 100644 index 000000000..07b383932 --- /dev/null +++ b/src/Sonarr.Api.V5/Settings/IndexerSettingsController.cs @@ -0,0 +1,30 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using Sonarr.Http; +using Sonarr.Http.Validation; + +namespace Sonarr.Api.V5.Settings +{ + [V5ApiController("settings/indexer")] + public class IndexerSettingsController : SettingsController<IndexerSettingsResource> + { + public IndexerSettingsController(IConfigFileProvider configFileProvider, + IConfigService configService) + : base(configFileProvider, configService) + { + SharedValidator.RuleFor(c => c.MinimumAge) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.Retention) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.RssSyncInterval) + .IsValidRssSyncInterval(); + } + + protected override IndexerSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model) + { + return IndexerConfigResourceMapper.ToResource(model); + } + } +} diff --git a/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs b/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs new file mode 100644 index 000000000..3c1ffaac5 --- /dev/null +++ b/src/Sonarr.Api.V5/Settings/IndexerSettingsResource.cs @@ -0,0 +1,27 @@ +using NzbDrone.Core.Configuration; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.Settings +{ + public class IndexerSettingsResource : RestResource + { + public int MinimumAge { get; set; } + public int Retention { get; set; } + public int MaximumSize { get; set; } + public int RssSyncInterval { get; set; } + } + + public static class IndexerConfigResourceMapper + { + public static IndexerSettingsResource ToResource(IConfigService model) + { + return new IndexerSettingsResource + { + MinimumAge = model.MinimumAge, + Retention = model.Retention, + MaximumSize = model.MaximumSize, + RssSyncInterval = model.RssSyncInterval + }; + } + } +} diff --git a/src/Sonarr.Api.V5/Settings/MediaManagementSettingsController.cs b/src/Sonarr.Api.V5/Settings/MediaManagementSettingsController.cs index 3d727bfa9..113db8ff4 100644 --- a/src/Sonarr.Api.V5/Settings/MediaManagementSettingsController.cs +++ b/src/Sonarr.Api.V5/Settings/MediaManagementSettingsController.cs @@ -11,16 +11,17 @@ namespace Sonarr.Api.V5.Settings; [V5ApiController("settings/mediamanagement")] public class MediaManagementSettingsController : SettingsController<MediaManagementSettingsResource> { - public MediaManagementSettingsController(IConfigService configService, - PathExistsValidator pathExistsValidator, - FolderChmodValidator folderChmodValidator, - FolderWritableValidator folderWritableValidator, - SeriesPathValidator seriesPathValidator, - StartupFolderValidator startupFolderValidator, - SystemFolderValidator systemFolderValidator, - RootFolderAncestorValidator rootFolderAncestorValidator, - RootFolderValidator rootFolderValidator) - : base(configService) + public MediaManagementSettingsController(IConfigFileProvider configFileProvider, + IConfigService configService, + PathExistsValidator pathExistsValidator, + FolderChmodValidator folderChmodValidator, + FolderWritableValidator folderWritableValidator, + SeriesPathValidator seriesPathValidator, + StartupFolderValidator startupFolderValidator, + SystemFolderValidator systemFolderValidator, + RootFolderAncestorValidator rootFolderAncestorValidator, + RootFolderValidator rootFolderValidator) + : base(configFileProvider, configService) { SharedValidator.RuleFor(c => c.RecycleBinCleanupDays).GreaterThanOrEqualTo(0); SharedValidator.RuleFor(c => c.ChmodFolder).SetValidator(folderChmodValidator).When(c => !string.IsNullOrEmpty(c.ChmodFolder) && (OsInfo.IsLinux || OsInfo.IsOsx)); @@ -62,7 +63,7 @@ public MediaManagementSettingsController(IConfigService configService, }); } - protected override MediaManagementSettingsResource ToResource(IConfigService model) + protected override MediaManagementSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model) { return MediaManagementConfigResourceMapper.ToResource(model); } diff --git a/src/Sonarr.Api.V5/Settings/NamingSettingsController.cs b/src/Sonarr.Api.V5/Settings/NamingSettingsController.cs index 5c0210fb6..69d4fa48b 100644 --- a/src/Sonarr.Api.V5/Settings/NamingSettingsController.cs +++ b/src/Sonarr.Api.V5/Settings/NamingSettingsController.cs @@ -1,5 +1,7 @@ using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; @@ -36,27 +38,24 @@ public NamingSettingsController(INamingConfigService namingConfigService, protected override NamingSettingsResource GetResourceById(int id) { - return GetNamingConfig(); + return _namingConfigService.GetConfig().ToResource(); } [HttpGet] - public NamingSettingsResource GetNamingConfig() + public Ok<NamingSettingsResource> GetNamingConfig() { - var nameSpec = _namingConfigService.GetConfig(); - var resource = nameSpec.ToResource(); - - return resource; + return TypedResults.Ok(GetResourceById(1)); } [RestPutById] - public ActionResult<NamingSettingsResource> UpdateNamingConfig([FromBody] NamingSettingsResource resource) + public Results<Accepted<NamingSettingsResource>, NotFound> UpdateNamingConfig([FromBody] NamingSettingsResource resource) { var nameSpec = resource.ToModel(); ValidateFormatResult(nameSpec); _namingConfigService.Save(nameSpec); - return Accepted(resource.Id); + return TypedAccepted(resource.Id); } [HttpGet("examples")] @@ -64,7 +63,7 @@ public object GetExamples([FromQuery]NamingSettingsResource settings) { if (settings.Id == 0) { - settings = GetNamingConfig(); + settings = GetResourceById(1); } var nameSpec = settings.ToModel(); diff --git a/src/Sonarr.Api.V5/Settings/SettingsController.cs b/src/Sonarr.Api.V5/Settings/SettingsController.cs index 51e4f702f..c37e2052c 100644 --- a/src/Sonarr.Api.V5/Settings/SettingsController.cs +++ b/src/Sonarr.Api.V5/Settings/SettingsController.cs @@ -1,4 +1,6 @@ using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Configuration; using Sonarr.Http.REST; @@ -9,41 +11,45 @@ namespace Sonarr.Api.V5.Settings public abstract class SettingsController<TResource> : RestController<TResource> where TResource : RestResource, new() { - protected readonly IConfigService _configService; + private readonly IConfigFileProvider _configFileProvider; + private readonly IConfigService _configService; - protected SettingsController(IConfigService configService) + protected SettingsController(IConfigFileProvider configFileProvider, IConfigService configService) { + _configFileProvider = configFileProvider; _configService = configService; } protected override TResource GetResourceById(int id) { - return GetConfig(); - } - - [HttpGet] - [Produces("application/json")] - public TResource GetConfig() - { - var resource = ToResource(_configService); - resource.Id = 1; + var resource = ToResource(_configFileProvider, _configService); + resource.Id = id; return resource; } + [HttpGet] + [Produces("application/json")] + public Ok<TResource> GetConfig() + { + return TypedResults.Ok(GetResourceById(1)); + } + [RestPutById] [Consumes("application/json")] - public virtual ActionResult<TResource> SaveConfig([FromBody] TResource resource) + [Produces("application/json")] + public virtual Results<Accepted<TResource>, NotFound> SaveSettings([FromBody] TResource resource) { var dictionary = resource.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public) .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + _configFileProvider.SaveConfigDictionary(dictionary); _configService.SaveConfigDictionary(dictionary); - return Accepted(resource.Id); + return TypedAccepted(resource.Id); } - protected abstract TResource ToResource(IConfigService model); + protected abstract TResource ToResource(IConfigFileProvider configFile, IConfigService model); } } diff --git a/src/Sonarr.Api.V5/Settings/UiSettingsController.cs b/src/Sonarr.Api.V5/Settings/UiSettingsController.cs index 383010ab7..8172adf1a 100644 --- a/src/Sonarr.Api.V5/Settings/UiSettingsController.cs +++ b/src/Sonarr.Api.V5/Settings/UiSettingsController.cs @@ -1,22 +1,16 @@ -using System.Reflection; using FluentValidation; -using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Configuration; using NzbDrone.Core.Languages; using Sonarr.Http; -using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V5.Settings; [V5ApiController("settings/ui")] public class UiSettingsController : SettingsController<UiSettingsResource> { - private readonly IConfigFileProvider _configFileProvider; - public UiSettingsController(IConfigFileProvider configFileProvider, IConfigService configService) - : base(configService) + : base(configFileProvider, configService) { - _configFileProvider = configFileProvider; SharedValidator.RuleFor(c => c.UiLanguage).Custom((value, context) => { if (!Language.All.Any(o => o.Id == value)) @@ -30,21 +24,8 @@ public UiSettingsController(IConfigFileProvider configFileProvider, IConfigServi .WithMessage("The UI Language value cannot be less than 1"); } - [RestPutById] - public override ActionResult<UiSettingsResource> SaveConfig([FromBody] UiSettingsResource resource) + protected override UiSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model) { - var dictionary = resource.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); - - _configFileProvider.SaveConfigDictionary(dictionary); - _configService.SaveConfigDictionary(dictionary); - - return Accepted(resource.Id); - } - - protected override UiSettingsResource ToResource(IConfigService model) - { - return UiSettingsResourceMapper.ToResource(_configFileProvider, model); + return UiSettingsResourceMapper.ToResource(configFile, model); } } diff --git a/src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs b/src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs index b44942bba..fc6af58d3 100644 --- a/src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs +++ b/src/Sonarr.Api.V5/Settings/UpdateSettingsController.cs @@ -1,50 +1,24 @@ -using System.Reflection; using FluentValidation; -using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Validation.Paths; using Sonarr.Http; -using Sonarr.Http.REST; namespace Sonarr.Api.V5.Settings; [V5ApiController("settings/update")] -public class UpdateSettingsController : RestController<UpdateSettingsResource> +public class UpdateSettingsController : SettingsController<UpdateSettingsResource> { - private readonly IConfigFileProvider _configFileProvider; - - public UpdateSettingsController(IConfigFileProvider configFileProvider) + public UpdateSettingsController(IConfigFileProvider configFileProvider, IConfigService configService) + : base(configFileProvider, configService) { - _configFileProvider = configFileProvider; SharedValidator.RuleFor(c => c.UpdateScriptPath) .IsValidPath() .When(c => c.UpdateMechanism == UpdateMechanism.Script); } - [HttpGet] - public UpdateSettingsResource GetUpdateSettings() + protected override UpdateSettingsResource ToResource(IConfigFileProvider configFile, IConfigService model) { - var resource = new UpdateSettingsResource - { - Branch = _configFileProvider.Branch, - UpdateAutomatically = _configFileProvider.UpdateAutomatically, - UpdateMechanism = _configFileProvider.UpdateMechanism, - UpdateScriptPath = _configFileProvider.UpdateScriptPath - }; - - return resource; - } - - [HttpPut] - public ActionResult<UpdateSettingsResource> SaveUpdateSettings([FromBody] UpdateSettingsResource resource) - { - var dictionary = resource.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); - - _configFileProvider.SaveConfigDictionary(dictionary); - - return Accepted(resource); + return UpdateSettingsResourceMapper.ToResource(configFile); } } diff --git a/src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs b/src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs index d17f2f12e..c8498ae1e 100644 --- a/src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs +++ b/src/Sonarr.Api.V5/Settings/UpdateSettingsResource.cs @@ -1,3 +1,4 @@ +using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using Sonarr.Http.REST; @@ -10,3 +11,17 @@ public class UpdateSettingsResource : RestResource public UpdateMechanism UpdateMechanism { get; set; } public string? UpdateScriptPath { get; set; } } + +public static class UpdateSettingsResourceMapper +{ + public static UpdateSettingsResource ToResource(IConfigFileProvider config) + { + return new UpdateSettingsResource + { + Branch = config.Branch, + UpdateAutomatically = config.UpdateAutomatically, + UpdateMechanism = config.UpdateMechanism, + UpdateScriptPath = config.UpdateScriptPath + }; + } +} diff --git a/src/Sonarr.Api.V5/Sonarr.Api.V5.csproj b/src/Sonarr.Api.V5/Sonarr.Api.V5.csproj index 9e3ba07bf..9e67c56e1 100644 --- a/src/Sonarr.Api.V5/Sonarr.Api.V5.csproj +++ b/src/Sonarr.Api.V5/Sonarr.Api.V5.csproj @@ -9,7 +9,7 @@ <PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="Ical.Net" Version="4.3.1" /> <PackageReference Include="NLog" Version="5.5.1" /> - <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" /> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" /> diff --git a/src/Sonarr.Api.V5/System/Backup/BackupController.cs b/src/Sonarr.Api.V5/System/Backup/BackupController.cs index 483d539c8..6c729d50d 100644 --- a/src/Sonarr.Api.V5/System/Backup/BackupController.cs +++ b/src/Sonarr.Api.V5/System/Backup/BackupController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Crypto; using NzbDrone.Common.Disk; @@ -29,7 +31,7 @@ public BackupController(IBackupService backupService, [HttpGet] [Produces("application/json")] - public ActionResult<List<BackupResource>> GetAll() + public Ok<List<BackupResource>> GetAll() { var backups = _backupService.GetBackups(); @@ -45,11 +47,11 @@ public ActionResult<List<BackupResource>> GetAll() .OrderByDescending(b => b.Time) .ToList(); - return resources; + return TypedResults.Ok(resources); } [RestDeleteById] - public ActionResult Delete(int id) + public Results<NoContent, NotFound> Delete(int id) { var backup = GetBackupById(id); @@ -67,30 +69,30 @@ public ActionResult Delete(int id) _diskProvider.DeleteFile(path); - return NoContent(); + return TypedResults.NoContent(); } [HttpPost("restore/{id:int}")] [Produces("application/json")] - public ActionResult<object> Restore([FromRoute] int id) + public Results<Ok<object>, NotFound> Restore([FromRoute] int id) { var backup = GetBackupById(id); if (backup == null) { - return NotFound(); + return TypedResults.NotFound(); } var path = GetBackupPath(backup); _backupService.Restore(path); - return new { RestartRequired = true }; + return TypedResults.Ok((object)new { RestartRequired = true }); } [HttpPost("restore/upload")] [Produces("application/json")] [RequestFormLimits(MultipartBodyLengthLimit = 5000000000)] - public ActionResult<object> RestoreUpload() + public Results<Ok<object>, BadRequest<object>> RestoreUpload() { var files = Request.Form.Files; @@ -104,7 +106,7 @@ public ActionResult<object> RestoreUpload() if (!ValidExtensions.Contains(extension)) { - return BadRequest(new { error = $"Invalid extension, must be one of: {string.Join(", ", ValidExtensions)}" }); + return TypedResults.BadRequest((object)new { error = $"Invalid extension, must be one of: {string.Join(", ", ValidExtensions)}" }); } var path = Path.Combine(_appFolderInfo.TempFolder, $"sonarr_backup_restore{extension}"); @@ -113,7 +115,7 @@ public ActionResult<object> RestoreUpload() _backupService.Restore(path); _diskProvider.DeleteFile(path); - return new { RestartRequired = true }; + return TypedResults.Ok((object)new { RestartRequired = true }); } private string GetBackupPath(NzbDrone.Core.Backup.Backup backup) diff --git a/src/Sonarr.Api.V5/System/SystemController.cs b/src/Sonarr.Api.V5/System/SystemController.cs index 26e525f19..1a1ffd44c 100644 --- a/src/Sonarr.Api.V5/System/SystemController.cs +++ b/src/Sonarr.Api.V5/System/SystemController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Internal; @@ -53,9 +55,9 @@ public SystemController(IAppFolderInfo appFolderInfo, [HttpGet("status")] [Produces("application/json")] - public SystemResource GetStatus() + public Ok<SystemResource> GetStatus() { - return new SystemResource + return TypedResults.Ok(new SystemResource { AppName = BuildInfo.AppName, InstanceName = _configFileProvider.InstanceName, @@ -88,39 +90,39 @@ public SystemResource GetStatus() PackageAuthor = _deploymentInfoProvider.PackageAuthor, PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism, PackageUpdateMechanismMessage = _deploymentInfoProvider.PackageUpdateMechanismMessage - }; + }); } [HttpGet("routes")] [Produces("application/json")] - public IActionResult GetRoutes() + public ContentHttpResult GetRoutes() { using (var sw = new StringWriter()) { _graphWriter.Write(_endpointData, sw); var graph = sw.ToString(); - return Content(graph, "text/plain"); + return TypedResults.Content(graph, "text/plain"); } } [HttpGet("routes/duplicate")] [Produces("application/json")] - public object DuplicateRoutes() + public Ok<Dictionary<string, List<string>>> DuplicateRoutes() { - return _detector.GetDuplicateEndpoints(_endpointData); + return TypedResults.Ok(_detector.GetDuplicateEndpoints(_endpointData)); } [HttpPost("shutdown")] - public object Shutdown() + public Ok<object> Shutdown() { Task.Factory.StartNew(() => _lifecycleService.Shutdown()); - return new { ShuttingDown = true }; + return TypedResults.Ok((object)new { ShuttingDown = true }); } [HttpPost("restart")] - public object Restart() + public Ok<object> Restart() { Task.Factory.StartNew(() => _lifecycleService.Restart()); - return new { Restarting = true }; + return TypedResults.Ok((object)new { Restarting = true }); } } diff --git a/src/Sonarr.Api.V5/System/Tasks/TaskController.cs b/src/Sonarr.Api.V5/System/Tasks/TaskController.cs index 77b103632..912b87603 100644 --- a/src/Sonarr.Api.V5/System/Tasks/TaskController.cs +++ b/src/Sonarr.Api.V5/System/Tasks/TaskController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; @@ -22,12 +24,12 @@ public TaskController(ITaskManager taskManager, IBroadcastSignalRMessage broadca [HttpGet] [Produces("application/json")] - public ActionResult<List<TaskResource>> GetAll() + public Ok<List<TaskResource>> GetAll() { - return _taskManager.GetAll() + return TypedResults.Ok(_taskManager.GetAll() .Select(ConvertToResource) .OrderBy(t => t.Name) - .ToList(); + .ToList()); } protected override TaskResource? GetResourceById(int id) diff --git a/src/Sonarr.Api.V5/Tags/TagController.cs b/src/Sonarr.Api.V5/Tags/TagController.cs index 395d2847f..c39db3dc3 100644 --- a/src/Sonarr.Api.V5/Tags/TagController.cs +++ b/src/Sonarr.Api.V5/Tags/TagController.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Datastore.Events; @@ -38,30 +40,32 @@ protected override TagResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<TagResource> GetAll() + public Ok<List<TagResource>> GetAll() { - return _tagService.All().ToResource(); + return TypedResults.Ok(_tagService.All().ToResource()); } [RestPostById] [Consumes("application/json")] - public ActionResult<TagResource> Create([FromBody] TagResource resource) + public Results<Created<TagResource>, NotFound> Create([FromBody] TagResource resource) { - return Created(_tagService.Add(resource.ToModel()).Id); + return TypedCreated(_tagService.Add(resource.ToModel()).Id); } [RestPutById] [Consumes("application/json")] - public ActionResult<TagResource> Update([FromBody] TagResource resource) + public Results<Accepted<TagResource>, NotFound> Update([FromBody] TagResource resource) { _tagService.Update(resource.ToModel()); - return Accepted(resource.Id); + return TypedAccepted(resource.Id); } [RestDeleteById] - public void DeleteTag(int id) + public NoContent DeleteTag(int id) { _tagService.Delete(id); + + return TypedResults.NoContent(); } [NonAction] diff --git a/src/Sonarr.Api.V5/Tags/TagDetailsController.cs b/src/Sonarr.Api.V5/Tags/TagDetailsController.cs index 5817f31ef..eb0a81577 100644 --- a/src/Sonarr.Api.V5/Tags/TagDetailsController.cs +++ b/src/Sonarr.Api.V5/Tags/TagDetailsController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.Tags; using Sonarr.Http; @@ -22,8 +24,8 @@ protected override TagDetailsResource GetResourceById(int id) [HttpGet] [Produces("application/json")] - public List<TagDetailsResource> GetAll() + public Ok<List<TagDetailsResource>> GetAll() { - return _tagService.Details().ToResource(); + return TypedResults.Ok(_tagService.Details().ToResource()); } } diff --git a/src/Sonarr.Api.V5/Update/UpdateController.cs b/src/Sonarr.Api.V5/Update/UpdateController.cs index a2d0cbabe..23c88e3a1 100644 --- a/src/Sonarr.Api.V5/Update/UpdateController.cs +++ b/src/Sonarr.Api.V5/Update/UpdateController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; @@ -23,7 +25,7 @@ public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHisto [HttpGet] [Produces("application/json")] - public List<UpdateResource> GetRecentUpdates() + public Ok<List<UpdateResource>> GetRecentUpdates() { var resources = _recentUpdateProvider.GetRecentUpdatePackages() .OrderByDescending(u => u.Version) @@ -48,7 +50,7 @@ public List<UpdateResource> GetRecentUpdates() if (!_configFileProvider.LogDbEnabled) { - return resources; + return TypedResults.Ok(resources); } var updateHistory = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate); @@ -65,7 +67,7 @@ public List<UpdateResource> GetRecentUpdates() } } - return resources; + return TypedResults.Ok(resources); } } } diff --git a/src/Sonarr.Api.V5/Wanted/CutoffController.cs b/src/Sonarr.Api.V5/Wanted/CutoffController.cs index cf9f3cece..426a3d189 100644 --- a/src/Sonarr.Api.V5/Wanted/CutoffController.cs +++ b/src/Sonarr.Api.V5/Wanted/CutoffController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -28,7 +30,7 @@ public CutoffController(IEpisodeCutoffService episodeCutoffService, [HttpGet] [Produces("application/json")] - public PagingResource<EpisodeResource> GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] CutoffSubresource[]? includeSubresources = null) + public Ok<PagingResource<EpisodeResource>> GetCutoffUnmetEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] CutoffSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource<EpisodeResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<EpisodeResource, Episode>( @@ -56,6 +58,6 @@ public PagingResource<EpisodeResource> GetCutoffUnmetEpisodes([FromQuery] Paging var resource = pagingSpec.ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, v => MapToResource(v, includeSeries, includeEpisodeFile, includeImages)); - return resource; + return TypedResults.Ok(resource); } } diff --git a/src/Sonarr.Api.V5/Wanted/MissingController.cs b/src/Sonarr.Api.V5/Wanted/MissingController.cs index d9cb6aee2..dfffae74c 100644 --- a/src/Sonarr.Api.V5/Wanted/MissingController.cs +++ b/src/Sonarr.Api.V5/Wanted/MissingController.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.Datastore; @@ -24,7 +26,7 @@ public MissingController(IEpisodeService episodeService, [HttpGet] [Produces("application/json")] - public PagingResource<EpisodeResource> GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] MissingSubresource[]? includeSubresources = null) + public Ok<PagingResource<EpisodeResource>> GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, bool includeSpecials = true, [FromQuery] MissingSubresource[]? includeSubresources = null) { var pagingResource = new PagingResource<EpisodeResource>(paging); var pagingSpec = pagingResource.MapToPagingSpec<EpisodeResource, Episode>( @@ -49,8 +51,8 @@ public PagingResource<EpisodeResource> GetMissingEpisodes([FromQuery] PagingRequ var includeSeries = includeSubresources.Contains(MissingSubresource.Series); var includeImages = includeSubresources.Contains(MissingSubresource.Images); - var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages)); + var resource = pagingSpec.ApplyToPage(spec => _episodeService.EpisodesWithoutFiles(spec, includeSpecials), v => MapToResource(v, includeSeries, false, includeImages)); - return resource; + return TypedResults.Ok(resource); } } diff --git a/src/Sonarr.Api.V5/openapi.json b/src/Sonarr.Api.V5/openapi.json index c421af9d5..ed7a967ae 100644 --- a/src/Sonarr.Api.V5/openapi.json +++ b/src/Sonarr.Api.V5/openapi.json @@ -623,12 +623,19 @@ ], "parameters": [ { - "name": "forceSave", + "name": "skipTesting", "in": "query", "schema": { "type": "boolean", "default": false } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } } ], "requestBody": { @@ -670,12 +677,19 @@ } }, { - "name": "forceSave", + "name": "skipTesting", "in": "query", "schema": { "type": "boolean", "default": false } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } } ], "requestBody": { @@ -779,11 +793,10 @@ ], "parameters": [ { - "name": "forceTest", + "name": "skipValidation", "in": "query", "schema": { - "type": "boolean", - "default": false + "$ref": "#/components/schemas/SkipValidation" } } ], @@ -1565,6 +1578,101 @@ } } }, + "/api/v5/settings/general/{id}": { + "put": { + "tags": [ + "GeneralSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "GeneralSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + } + } + } + } + } + }, + "/api/v5/settings/general": { + "get": { + "tags": [ + "GeneralSettings" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralSettingsResource" + } + } + } + } + } + } + }, "/api/v5/health": { "get": { "tags": [ @@ -1936,6 +2044,724 @@ } } }, + "/api/v5/importlistexclusion": { + "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": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResourcePagingResource" + } + } + } + } + } + }, + "post": { + "tags": [ + "ImportListExclusion" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + } + } + } + } + } + }, + "/api/v5/importlistexclusion/{id}": { + "put": { + "tags": [ + "ImportListExclusion" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "ImportListExclusion" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "tags": [ + "ImportListExclusion" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResource" + } + } + } + } + } + } + }, + "/api/v5/importlistexclusion/bulk": { + "delete": { + "tags": [ + "ImportListExclusion" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/indexer": { + "get": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "skipTesting", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "/api/v5/indexer/{id}": { + "put": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "skipTesting", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + }, + "/api/v5/indexer/bulk": { + "put": { + "tags": [ + "Indexer" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Indexer" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerBulkResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/indexer/schema": { + "get": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + } + } + } + } + }, + "/api/v5/indexer/test": { + "post": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/indexer/testall": { + "post": { + "tags": [ + "Indexer" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/indexer/action/{name}": { + "post": { + "tags": [ + "Indexer" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/indexerflag": { + "get": { + "tags": [ + "IndexerFlag" + ], + "responses": { + "200": { + "description": "OK", + "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/v5/settings/indexer": { + "get": { + "tags": [ + "IndexerSettings" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + } + } + } + } + } + }, + "/api/v5/settings/indexer/{id}": { + "put": { + "tags": [ + "IndexerSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "IndexerSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IndexerSettingsResource" + } + } + } + } + } + } + }, + "/api/v5/language": { + "get": { + "tags": [ + "Language" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LanguageResource" + } + } + } + } + } + } + } + }, + "/api/v5/language/{id}": { + "get": { + "tags": [ + "Language" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LanguageResource" + } + } + } + } + } + } + }, "/api/v5/localization": { "get": { "tags": [ @@ -2222,6 +3048,364 @@ } } }, + "/api/v5/settings/mediamanagement": { + "get": { + "tags": [ + "MediaManagementSettings" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + } + } + } + } + } + }, + "/api/v5/settings/mediamanagement/{id}": { + "put": { + "tags": [ + "MediaManagementSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + } + } + } + } + }, + "get": { + "tags": [ + "MediaManagementSettings" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaManagementSettingsResource" + } + } + } + } + } + } + }, + "/api/v5/metadata": { + "get": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "skipTesting", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "/api/v5/metadata/{id}": { + "put": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "skipTesting", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + }, + "/api/v5/metadata/schema": { + "get": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + } + } + } + } + }, + "/api/v5/metadata/test": { + "post": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "skipValidation", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SkipValidation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/metadata/testall": { + "post": { + "tags": [ + "Metadata" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v5/metadata/action/{name}": { + "post": { + "tags": [ + "Metadata" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataResource" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v5/wanted/missing": { "get": { "tags": [ @@ -4822,6 +6006,25 @@ } } }, + "/api/v5/settings/ui": { + "get": { + "tags": [ + "UiSettings" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UiSettingsResource" + } + } + } + } + } + } + }, "/api/v5/settings/ui/{id}": { "put": { "tags": [ @@ -4898,25 +6101,6 @@ } } }, - "/api/v5/settings/ui": { - "get": { - "tags": [ - "UiSettings" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UiSettingsResource" - } - } - } - } - } - } - }, "/api/v5/update": { "get": { "tags": [ @@ -4993,45 +6177,37 @@ "200": { "description": "OK", "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/UpdateSettingsResource" - } - }, "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSettingsResource" } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSettingsResource" - } } } } } - }, + } + }, + "/api/v5/settings/update/{id}": { "put": { "tags": [ "UpdateSettings" ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateSettingsResource" } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSettingsResource" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/UpdateSettingsResource" - } } } }, @@ -5057,9 +6233,7 @@ } } } - } - }, - "/api/v5/settings/update/{id}": { + }, "get": { "tags": [ "UpdateSettings" @@ -5149,6 +6323,13 @@ ], "type": "string" }, + "AuthenticationRequiredType": { + "enum": [ + "enabled", + "disabledForLocalAddresses" + ], + "type": "string" + }, "AuthenticationType": { "enum": [ "none", @@ -5329,6 +6510,14 @@ ], "type": "string" }, + "CertificateValidationType": { + "enum": [ + "enabled", + "disabledForLocalAddresses", + "disabled" + ], + "type": "string" + }, "Command": { "type": "object", "properties": { @@ -5881,7 +7070,8 @@ "diskCustomFormatScore", "diskCustomFormatScoreIncrement", "diskUpgradesNotAllowed", - "diskNotUpgrade" + "diskNotUpgrade", + "beforeAirDate" ], "type": "string" }, @@ -6157,6 +7347,14 @@ ], "type": "string" }, + "EpisodeTitleRequiredType": { + "enum": [ + "always", + "bulkSeasonReleases", + "never" + ], + "type": "string" + }, "EpisodesMonitoredResource": { "required": [ "episodeIds" @@ -6250,6 +7448,161 @@ }, "additionalProperties": false }, + "FileDateType": { + "enum": [ + "none", + "localAirDate", + "utcAirDate" + ], + "type": "string" + }, + "GeneralSettingsResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "bindAddress": { + "type": "string", + "nullable": true + }, + "port": { + "type": "integer", + "format": "int32" + }, + "sslPort": { + "type": "integer", + "format": "int32" + }, + "enableSsl": { + "type": "boolean" + }, + "launchBrowser": { + "type": "boolean" + }, + "authenticationMethod": { + "$ref": "#/components/schemas/AuthenticationType" + }, + "authenticationRequired": { + "$ref": "#/components/schemas/AuthenticationRequiredType" + }, + "analyticsEnabled": { + "type": "boolean" + }, + "username": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + }, + "passwordConfirmation": { + "type": "string", + "nullable": true + }, + "logLevel": { + "type": "string", + "nullable": true + }, + "logSizeLimit": { + "type": "integer", + "format": "int32" + }, + "consoleLogLevel": { + "type": "string", + "nullable": true + }, + "branch": { + "type": "string", + "nullable": true + }, + "apiKey": { + "type": "string", + "nullable": true + }, + "sslCertPath": { + "type": "string", + "nullable": true + }, + "sslKeyPath": { + "type": "string", + "nullable": true + }, + "sslCertPassword": { + "type": "string", + "nullable": true + }, + "urlBase": { + "type": "string", + "nullable": true + }, + "instanceName": { + "type": "string", + "nullable": true + }, + "applicationUrl": { + "type": "string", + "nullable": true + }, + "updateAutomatically": { + "type": "boolean" + }, + "updateMechanism": { + "$ref": "#/components/schemas/UpdateMechanism" + }, + "updateScriptPath": { + "type": "string", + "nullable": true + }, + "proxyEnabled": { + "type": "boolean" + }, + "proxyType": { + "$ref": "#/components/schemas/ProxyType" + }, + "proxyHostname": { + "type": "string", + "nullable": true + }, + "proxyPort": { + "type": "integer", + "format": "int32" + }, + "proxyUsername": { + "type": "string", + "nullable": true + }, + "proxyPassword": { + "type": "string", + "nullable": true + }, + "proxyBypassFilter": { + "type": "string", + "nullable": true + }, + "proxyBypassLocalAddresses": { + "type": "boolean" + }, + "certificateValidation": { + "$ref": "#/components/schemas/CertificateValidationType" + }, + "backupFolder": { + "type": "string", + "nullable": true + }, + "backupInterval": { + "type": "integer", + "format": "int32" + }, + "backupRetention": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "HealthCheckReason": { "enum": [ "appDataLocation", @@ -6469,6 +7822,74 @@ ], "type": "string" }, + "ImportListExclusionBulkResource": { + "required": [ + "ids" + ], + "type": "object", + "properties": { + "ids": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ImportListExclusionResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "tvdbId": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string", + "nullable": true + } + }, + "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 + }, "ImportRejectionReason": { "enum": [ "unknown", @@ -6505,7 +7926,9 @@ "unverifiedSceneMapping", "notQualityUpgrade", "notRevisionUpgrade", - "notCustomFormatUpgrade" + "notCustomFormatUpgrade", + "notCustomFormatUpgradeAfterRename", + "multiSeason" ], "type": "string" }, @@ -6525,6 +7948,184 @@ }, "additionalProperties": false }, + "IndexerBulkResource": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "applyTags": { + "$ref": "#/components/schemas/ApplyTags" + }, + "enableRss": { + "type": "boolean", + "nullable": true + }, + "enableAutomaticSearch": { + "type": "boolean", + "nullable": true + }, + "enableInteractiveSearch": { + "type": "boolean", + "nullable": true + }, + "priority": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "seasonSearchMaximumSingleEpisodeAge": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "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": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexerResource" + }, + "nullable": true + }, + "enableRss": { + "type": "boolean" + }, + "enableAutomaticSearch": { + "type": "boolean" + }, + "enableInteractiveSearch": { + "type": "boolean" + }, + "supportsRss": { + "type": "boolean" + }, + "supportsSearch": { + "type": "boolean" + }, + "protocol": { + "$ref": "#/components/schemas/DownloadProtocol" + }, + "priority": { + "type": "integer", + "format": "int32" + }, + "seasonSearchMaximumSingleEpisodeAge": { + "type": "integer", + "format": "int32" + }, + "downloadClientId": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "IndexerSettingsResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "minimumAge": { + "type": "integer", + "format": "int32" + }, + "retention": { + "type": "integer", + "format": "int32" + }, + "maximumSize": { + "type": "integer", + "format": "int32" + }, + "rssSyncInterval": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "Language": { "type": "object", "properties": { @@ -6539,6 +8140,25 @@ }, "additionalProperties": false }, + "LanguageResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "nameLower": { + "type": "string", + "nullable": true, + "readOnly": true + } + }, + "additionalProperties": false + }, "LocalizationLanguageResource": { "type": "object", "properties": { @@ -6680,6 +8300,10 @@ "type": "string", "nullable": true }, + "relativePath": { + "type": "string", + "nullable": true + }, "seriesId": { "type": "integer", "format": "int32" @@ -7000,6 +8624,153 @@ }, "additionalProperties": false }, + "MediaManagementSettingsResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "autoUnmonitorPreviouslyDownloadedEpisodes": { + "type": "boolean" + }, + "recycleBin": { + "type": "string", + "nullable": true + }, + "recycleBinCleanupDays": { + "type": "integer", + "format": "int32" + }, + "downloadPropersAndRepacks": { + "$ref": "#/components/schemas/ProperDownloadTypes" + }, + "createEmptySeriesFolders": { + "type": "boolean" + }, + "deleteEmptyFolders": { + "type": "boolean" + }, + "fileDate": { + "$ref": "#/components/schemas/FileDateType" + }, + "rescanAfterRefresh": { + "$ref": "#/components/schemas/RescanAfterRefreshType" + }, + "setPermissionsLinux": { + "type": "boolean" + }, + "chmodFolder": { + "type": "string", + "nullable": true + }, + "chownGroup": { + "type": "string", + "nullable": true + }, + "episodeTitleRequired": { + "$ref": "#/components/schemas/EpisodeTitleRequiredType" + }, + "skipFreeSpaceCheckWhenImporting": { + "type": "boolean" + }, + "minimumFreeSpaceWhenImporting": { + "type": "integer", + "format": "int32" + }, + "copyUsingHardlinks": { + "type": "boolean" + }, + "useScriptImport": { + "type": "boolean" + }, + "scriptImportPath": { + "type": "string", + "nullable": true + }, + "importExtraFiles": { + "type": "boolean" + }, + "extraFileExtensions": { + "type": "string", + "nullable": true + }, + "enableMediaInfo": { + "type": "boolean" + }, + "userRejectedExtensions": { + "type": "string", + "nullable": true + }, + "seasonPackUpgrade": { + "$ref": "#/components/schemas/SeasonPackUpgradeType" + }, + "seasonPackUpgradeThreshold": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "MetadataResource": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "nullable": true + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + }, + "nullable": true + }, + "implementationName": { + "type": "string", + "nullable": true + }, + "implementation": { + "type": "string", + "nullable": true + }, + "configContract": { + "type": "string", + "nullable": true + }, + "infoLink": { + "type": "string", + "nullable": true + }, + "message": { + "$ref": "#/components/schemas/ProviderMessage" + }, + "tags": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetadataResource" + }, + "nullable": true + }, + "enable": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "MissingSubresource": { "enum": [ "series", @@ -7394,6 +9165,14 @@ }, "additionalProperties": false }, + "ProperDownloadTypes": { + "enum": [ + "preferAndUpgrade", + "doNotUpgrade", + "doNotPrefer" + ], + "type": "string" + }, "ProviderMessage": { "type": "object", "properties": { @@ -7415,6 +9194,14 @@ ], "type": "string" }, + "ProxyType": { + "enum": [ + "http", + "socks4", + "socks5" + ], + "type": "string" + }, "Quality": { "type": "object", "properties": { @@ -8050,6 +9837,13 @@ }, "nullable": true }, + "airDateRestriction": { + "type": "boolean" + }, + "airDateGracePeriod": { + "type": "integer", + "format": "int32" + }, "indexerIds": { "type": "array", "items": { @@ -8337,6 +10131,14 @@ }, "additionalProperties": false }, + "RescanAfterRefreshType": { + "enum": [ + "always", + "afterManual", + "never" + ], + "type": "string" + }, "Revision": { "type": "object", "properties": { @@ -8420,6 +10222,14 @@ }, "additionalProperties": false }, + "SeasonPackUpgradeType": { + "enum": [ + "all", + "threshold", + "any" + ], + "type": "string" + }, "SeasonPassResource": { "type": "object", "properties": { @@ -8789,6 +10599,10 @@ }, "nullable": true }, + "originalCountry": { + "type": "string", + "nullable": true + }, "tags": { "uniqueItems": true, "type": "array", @@ -8908,6 +10722,14 @@ ], "type": "string" }, + "SkipValidation": { + "enum": [ + "none", + "warnings", + "all" + ], + "type": "string" + }, "SortDirection": { "enum": [ "default", @@ -9475,12 +11297,30 @@ { "name": "FileSystem" }, + { + "name": "GeneralSettings" + }, { "name": "Health" }, { "name": "History" }, + { + "name": "ImportListExclusion" + }, + { + "name": "Indexer" + }, + { + "name": "IndexerFlag" + }, + { + "name": "IndexerSettings" + }, + { + "name": "Language" + }, { "name": "Localization" }, @@ -9493,6 +11333,12 @@ { "name": "ManualImport" }, + { + "name": "MediaManagementSettings" + }, + { + "name": "Metadata" + }, { "name": "Missing" }, diff --git a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs index 16ff47cf3..0dfc5cb28 100644 --- a/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationBuilderExtensions.cs @@ -1,11 +1,11 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Diacritical; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; diff --git a/src/Sonarr.Http/REST/RestController.cs b/src/Sonarr.Http/REST/RestController.cs index ee32c6a09..e9da21178 100644 --- a/src/Sonarr.Http/REST/RestController.cs +++ b/src/Sonarr.Http/REST/RestController.cs @@ -4,6 +4,8 @@ using System.Reflection; using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; @@ -50,15 +52,15 @@ protected RestController() [RestGetById] [Produces("application/json")] - public virtual ActionResult<TResource> GetResourceByIdWithErrorHandler(int id) + public virtual Results<Ok<TResource>, NotFound> GetResourceByIdWithErrorHandler(int id) { try { - return GetResourceById(id); + return TypedResults.Ok(GetResourceById(id)); } catch (ModelNotFoundException) { - return NotFound(); + return TypedResults.NotFound(); } } @@ -156,6 +158,20 @@ protected void ValidateResource(TResource resource, bool validateId = false, boo } } + protected Results<Accepted<TResource>, NotFound> TypedAccepted(int id) + { + var result = GetResourceById(id); + + return TypedResults.Accepted(Url.Action(nameof(GetResourceByIdWithErrorHandler), new { id }), result); + } + + protected Results<Created<TResource>, NotFound> TypedCreated(int id) + { + var result = GetResourceById(id); + + return TypedResults.Created(Url.Action(nameof(GetResourceByIdWithErrorHandler), new { id }), result); + } + protected ActionResult<TResource> Accepted(int id) { var result = GetResourceById(id); diff --git a/yarn.lock b/yarn.lock index bb00f7087..16c3ef5a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,17 +3,18 @@ "@adobe/css-tools@^4.0.1": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" - integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== + version "4.4.4" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== "@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== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: - "@babel/highlight" "^7.25.7" - picocolors "^1.0.0" + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" "@babel/code-frame@^7.27.1": version "7.27.1" @@ -273,21 +274,16 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@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.7", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== "@babel/helper-validator-identifier@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== -"@babel/helper-validator-identifier@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" - integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== - "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -310,16 +306,6 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.4" -"@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.25.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - "@babel/parser@^7.27.1", "@babel/parser@^7.27.2": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.2.tgz#577518bedb17a2ce4212afd052e01f7df0941127" @@ -1076,21 +1062,16 @@ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@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== + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.3" -"@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== +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -1203,19 +1184,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - -"@jridgewell/gen-mapping@^0.3.12": +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== @@ -1223,15 +1192,6 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" -"@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.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - "@jridgewell/remapping@^2.3.5": version "2.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" @@ -1245,38 +1205,20 @@ 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.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.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== dependencies: "@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.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@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" - -"@jridgewell/trace-mapping@^0.3.28": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -1328,6 +1270,95 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@parcel/watcher-android-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" + integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A== + +"@parcel/watcher-darwin-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz#88d3e720b59b1eceffce98dac46d7c40e8be5e8e" + integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA== + +"@parcel/watcher-darwin-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063" + integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg== + +"@parcel/watcher-freebsd-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53" + integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng== + +"@parcel/watcher-linux-arm-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a" + integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ== + +"@parcel/watcher-linux-arm-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152" + integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg== + +"@parcel/watcher-linux-arm64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809" + integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA== + +"@parcel/watcher-linux-arm64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4" + integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA== + +"@parcel/watcher-linux-x64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639" + integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ== + +"@parcel/watcher-linux-x64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2" + integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg== + +"@parcel/watcher-win32-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e" + integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q== + +"@parcel/watcher-win32-ia32@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d" + integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g== + +"@parcel/watcher-win32-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d" + integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw== + +"@parcel/watcher@^2.4.1": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.6.tgz#3f932828c894f06d0ad9cfefade1756ecc6ef1f1" + integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.3" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.6" + "@parcel/watcher-darwin-arm64" "2.5.6" + "@parcel/watcher-darwin-x64" "2.5.6" + "@parcel/watcher-freebsd-x64" "2.5.6" + "@parcel/watcher-linux-arm-glibc" "2.5.6" + "@parcel/watcher-linux-arm-musl" "2.5.6" + "@parcel/watcher-linux-arm64-glibc" "2.5.6" + "@parcel/watcher-linux-arm64-musl" "2.5.6" + "@parcel/watcher-linux-x64-glibc" "2.5.6" + "@parcel/watcher-linux-x64-musl" "2.5.6" + "@parcel/watcher-win32-arm64" "2.5.6" + "@parcel/watcher-win32-ia32" "2.5.6" + "@parcel/watcher-win32-x64" "2.5.6" + "@react-dnd/asap@^5.0.1": version "5.0.2" resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" @@ -1449,10 +1480,26 @@ dependencies: "@types/readdir-glob" "*" -"@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/eslint-scope@^3.7.7": + 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 "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/history@^4.7.11": version "4.7.11" @@ -1472,7 +1519,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.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@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== @@ -1482,10 +1529,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@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/lodash@4.14.202": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== "@types/minimist@^1.2.0": version "1.2.5" @@ -1505,11 +1552,11 @@ integrity sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw== "@types/node@*": - version "22.7.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" - integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + version "25.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.3.tgz#605862544ee7ffd7a936bcbf0135a14012f1e549" + integrity sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ== dependencies: - undici-types "~6.19.2" + undici-types "~7.18.0" "@types/node@20.16.11": version "20.16.11" @@ -1782,129 +1829,129 @@ eslint-visitor-keys "^4.2.0" "@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== + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@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== +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - 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-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== -"@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-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== -"@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== +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== -"@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== +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/helper-wasm-bytecode" "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/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" -"@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== +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== dependencies: - "@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/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" -"@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== +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" -"@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== +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== dependencies: - "@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/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" -"@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== +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: - "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^2.1.1": @@ -1939,20 +1986,20 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -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-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.15.0, acorn@^8.9.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== add-px-to-style@1.0.0: version "1.0.0" @@ -1987,16 +2034,26 @@ ajv-keywords@^5.1.0: fast-deep-equal "^3.1.3" 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== + version "6.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.9.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc" + integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ajv@^8.0.1: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -2275,15 +2332,20 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== baseline-browser-mapping@^2.9.0: - version "2.9.11" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz#53724708c8db5f97206517ecfe362dbe5181deea" - integrity sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ== + version "2.10.0" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" + integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== big.js@^5.2.2: version "5.2.2" @@ -2320,20 +2382,27 @@ boolbase@^1.0.0: integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== +brace-expansion@^2.0.1, brace-expansion@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" +brace-expansion@^5.0.2: + version "5.0.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" + integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg== + dependencies: + balanced-match "^4.0.2" + braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -2341,7 +2410,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.21.10, browserslist@^4.23.3, browserslist@^4.24.0: +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== @@ -2351,7 +2420,7 @@ browserslist@^4.21.10, browserslist@^4.23.3, browserslist@^4.24.0: node-releases "^2.0.18" update-browserslist-db "^1.1.0" -browserslist@^4.28.0: +browserslist@^4.28.0, browserslist@^4.28.1: version "4.28.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== @@ -2385,6 +2454,14 @@ bytes@1: resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + 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" @@ -2396,6 +2473,14 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2429,11 +2514,11 @@ camelcase@^5.3.1: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663, caniuse-lite@^1.0.30001759: - version "1.0.30001761" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz" - integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g== + version "1.0.30001776" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz" + integrity sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw== -chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2466,9 +2551,9 @@ chokidar@^3.5.3: 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== + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== dependencies: readdirp "^4.0.1" @@ -2684,7 +2769,16 @@ crc32-stream@^4.0.2: crc-32 "^1.2.0" readable-stream "^3.4.0" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +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== @@ -2750,9 +2844,9 @@ css-tree@^2.3.1: source-map-js "^1.0.1" css-what@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== cssesc@^3.0.0: version "3.0.0" @@ -2764,11 +2858,6 @@ csstype@^3.0.2: 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" @@ -2803,14 +2892,14 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0: 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.3" -debug@^4.4.1: +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2872,6 +2961,11 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + 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" @@ -2967,14 +3061,23 @@ dot-case@^3.0.4: tslib "^2.0.3" dotenv@^16.0.3: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== + version "1.5.307" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz#09f8973100c39fb0d003b890393cd1d58932b1c8" + integrity sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg== electron-to-chromium@^1.5.28: version "1.5.35" @@ -2997,13 +3100,13 @@ emojis-list@^3.0.0: integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== 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" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== dependencies: once "^1.4.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: +enhanced-resolve@^5.0.0: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== @@ -3011,6 +3114,14 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.19.0: + version "5.20.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d" + integrity sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.0" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -3029,9 +3140,9 @@ errno@^0.1.1: prr "~1.0.1" error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== dependencies: is-arrayish "^0.2.1" @@ -3108,6 +3219,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + 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" @@ -3133,15 +3249,15 @@ es-iterator-helpers@^1.0.19: iterator.prototype "^1.1.3" safe-array-concat "^1.1.2" -es-module-lexer@^1.2.1: - 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-module-lexer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.0.0.tgz#f657cd7a9448dcdda9c070a3cb75e5dc1e85f5b1" + integrity sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw== -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" @@ -3308,15 +3424,15 @@ 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.4.3: +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-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== + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== eslint@8.57.1: version "8.57.1" @@ -3372,9 +3488,9 @@ espree@^9.6.0, espree@^9.6.1: eslint-visitor-keys "^3.4.1" esquery@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== dependencies: estraverse "^5.1.0" @@ -3425,7 +3541,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.3.2: +fast-glob@^3.2.11: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -3436,6 +3552,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -3447,9 +3574,9 @@ fast-levenshtein@^2.0.6: 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== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" @@ -3457,9 +3584,9 @@ fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + version "1.20.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== dependencies: reusify "^1.0.4" @@ -3566,9 +3693,9 @@ flat@^5.0.2: 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== + version "3.3.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.4.tgz#0986e681008f0f13f58e18656c47967682db5ff6" + integrity sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA== focus-lock@^0.11.6: version "0.11.6" @@ -3622,9 +3749,9 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: universalify "^2.0.0" fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" + integrity sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw== fs.realpath@^1.0.0: version "1.0.0" @@ -3677,11 +3804,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.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-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + 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" @@ -3710,14 +3861,14 @@ 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@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.0.tgz#9d9233a4a274fc28ef7adce5508b7ef6237a1be3" - integrity sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== +glob@^13.0.3: + version "13.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d" + integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== dependencies: - minimatch "^10.1.1" - minipass "^7.1.2" - path-scurry "^2.0.0" + minimatch "^10.2.2" + minipass "^7.1.3" + path-scurry "^2.0.2" glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: version "7.2.3" @@ -3791,6 +3942,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + 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" @@ -3838,6 +3994,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + 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" @@ -3906,10 +4067,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.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== +html-webpack-plugin@5.6.6: + version "5.6.6" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz#5321b9579f4a1949318550ced99c2a4a4e60cbaf" + integrity sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -3964,12 +4125,25 @@ immediate@~3.0.5: 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.7" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" - integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== +"immutable@^3.8.1 || ^4.0.0": + version "4.3.8" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7" + integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw== -import-fresh@^3.2.1, import-fresh@^3.3.0: +immutable@^5.0.2: + version "5.1.5" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" + integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +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== @@ -4326,9 +4500,9 @@ jquery@3.7.1: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -4375,9 +4549,9 @@ json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" optionalDependencies: @@ -4428,9 +4602,9 @@ lazystream@^1.0.0: readable-stream "^2.0.5" less@^4.1.3: - version "4.2.0" - resolved "https://registry.yarnpkg.com/less/-/less-4.2.0.tgz#cbefbfaa14a4cd388e2099b2b51f956e1465c450" - integrity sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA== + version "4.5.1" + resolved "https://registry.yarnpkg.com/less/-/less-4.5.1.tgz#739266532249a3de232e8b60ffb1b27ad5ec6ad8" + integrity sha512-UKgI3/KON4u6ngSsnDADsUERqhZknsVZbnuzlRZXLQCmfC/MDld42fTydUE9B+Mla1AL6SJ/Pp6SlEFi/AVGfw== dependencies: copy-anything "^2.0.1" parse-node-version "^1.0.1" @@ -4486,10 +4660,10 @@ livereload-js@^2.3.0: resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== +loader-runner@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" + integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== loader-utils@^1.2.3: version "1.4.2" @@ -4607,7 +4781,12 @@ 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.20, lodash@^4.17.21: +lodash@4.17.23, lodash@^4.17.20, lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + +lodash@^4.17.14: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4627,9 +4806,9 @@ lower-case@^2.0.2: tslib "^2.0.3" lru-cache@^11.0.0: - version "11.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.4.tgz#ecb523ebb0e6f4d837c807ad1abaea8e0619770d" - integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== + version "11.2.6" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.6.tgz#356bf8a29e88a7a2945507b31f6429a65a192c58" + integrity sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ== lru-cache@^5.1.1: version "5.1.1" @@ -4653,13 +4832,6 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.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== - dependencies: - semver "^6.0.0" - map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -4670,6 +4842,11 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" @@ -4720,7 +4897,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -4745,11 +4922,6 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@~2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -4771,40 +4943,33 @@ mini-css-extract-plugin@2.9.1: schema-utils "^4.0.0" tapable "^2.2.1" -minimatch@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== +minimatch@^10.2.2: + version "10.2.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" + integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== dependencies: - "@isaacs/brace-expansion" "^5.0.0" + brace-expansion "^5.0.2" 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" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== dependencies: brace-expansion "^1.1.7" minimatch@^5.1.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== 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== + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== 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" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.2" minimist-options@4.1.0: version "4.1.0" @@ -4820,10 +4985,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@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +minipass@^7.1.2, minipass@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== mkdirp@^0.5.6: version "0.5.6" @@ -4859,10 +5024,10 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^3.3.11, nanoid@^3.3.7: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare@^1.4.0: version "1.4.0" @@ -4895,6 +5060,11 @@ 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-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -4908,9 +5078,9 @@ node-releases@^2.0.18: integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== node-releases@^2.0.27: - version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== + version "2.0.36" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" + integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== normalize-package-data@^2.5.0: version "2.5.0" @@ -4969,6 +5139,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -5162,10 +5337,10 @@ 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@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.1.tgz#4b6572376cfd8b811fca9cd1f5c24b3cbac0fe10" - integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== +path-scurry@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85" + integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg== dependencies: lru-cache "^11.0.0" minipass "^7.1.2" @@ -5187,21 +5362,26 @@ 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, 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== - -picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== +picocolors@^1.0.1: + 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" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -5296,20 +5476,20 @@ postcss-modules-extract-imports@^3.0.0: integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.0: - 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== + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== dependencies: icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" + postcss-selector-parser "^7.0.0" postcss-value-parser "^4.1.0" postcss-modules-scope@^3.0.0: - 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== + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== dependencies: - postcss-selector-parser "^6.0.4" + postcss-selector-parser "^7.0.0" postcss-modules-values@^4.0.0: version "4.0.0" @@ -5335,7 +5515,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.12, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.1.1: +postcss-selector-parser@^6.0.12, 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== @@ -5343,6 +5523,14 @@ postcss-selector-parser@^6.0.12, postcss-selector-parser@^6.0.2, postcss-selecto cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz#e75d2e0d843f620e5df69076166f4e16f891cb9f" + integrity sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg== + 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" @@ -5353,16 +5541,6 @@ postcss-sorting@^8.0.2: resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-8.0.2.tgz#6393385ece272baf74bee9820fb1b58098e4eeca" integrity sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q== -postcss-url@10.1.3: - version "10.1.3" - resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-10.1.3.tgz#54120cc910309e2475ec05c2cfa8f8a2deafdf1e" - integrity sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw== - dependencies: - make-dir "~3.1.0" - mime "~2.5.2" - minimatch "~3.0.4" - xxhashjs "~0.2.2" - postcss-value-parser@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" @@ -5373,7 +5551,7 @@ 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.47, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.32: +postcss@8.4.47, postcss@^8.4.19, 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== @@ -5391,6 +5569,15 @@ postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^8.0.0, postcss@^8.4.21: + version "8.5.8" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" + integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prefix-style@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06" @@ -5452,7 +5639,14 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.13.0, qs@^6.4.0: +qs@6.15.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3" + integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== + dependencies: + side-channel "^1.1.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== @@ -5481,13 +5675,6 @@ raf@^3.1.0: dependencies: performance-now "^2.1.0" -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - raw-body@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425" @@ -5785,9 +5972,9 @@ readdir-glob@^1.1.2: 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== + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== readdirp@~3.6.0: version "3.6.0" @@ -6023,21 +6210,21 @@ resolve@^2.0.0-next.5: supports-preserve-symlinks-flag "^1.0.0" reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rgb@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" integrity sha512-F49dXX73a92N09uQkfCp2QjwXpmJcn9/i9PvjmwsSIXUGqRLCf/yx5Q9gRxuLQTq248kakqQuc8GX/U/CxSqlA== -rimraf@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.1.2.tgz#9a0f3cea2ab853e81291127422116ecf2a86ae89" - integrity sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g== +rimraf@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.1.3.tgz#afbee236b3bd2be331d4e7ce4493bac1718981af" + integrity sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA== dependencies: - glob "^13.0.0" + glob "^13.0.3" package-json-from-dist "^1.0.1" rimraf@^3.0.2: @@ -6064,7 +6251,7 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6094,18 +6281,20 @@ safe-regex-test@^1.0.3: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.58.3: - version "1.79.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.4.tgz#f9c45af35fbeb53d2c386850ec842098d9935267" - integrity sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg== + version "1.97.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.3.tgz#9cb59339514fa7e2aec592b9700953ac6e331ab2" + integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg== dependencies: chokidar "^4.0.0" - immutable "^4.0.0" + immutable "^5.0.2" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.5.0.tgz#b5549b671069b7aa392df55ec7574cf411179eb8" + integrity sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA== sax@~1.2.4: version "1.2.4" @@ -6119,7 +6308,7 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@>1.0.0, schema-utils@^4.0.0: +schema-utils@>1.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== @@ -6129,7 +6318,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.2.0: +schema-utils@^3.0.0, schema-utils@^3.1.1: 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== @@ -6138,6 +6327,16 @@ schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^4.0.0, schema-utils@^4.3.0, schema-utils@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + seamless-immutable@^7.1.3: version "7.1.4" resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8" @@ -6153,22 +6352,20 @@ section-iterator@^2.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.1: +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.8, semver@^7.6.0: +semver@^7.3.4, semver@^7.3.8: 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" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" +semver@^7.3.5, semver@^7.6.0: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== set-cookie-parser@^2.4.8: version "2.7.2" @@ -6226,6 +6423,35 @@ 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-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + 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" @@ -6236,6 +6462,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -6278,7 +6515,12 @@ 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.4: +source-map@^0.7.3: + version "0.7.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== + +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== @@ -6586,7 +6828,12 @@ table@^6.8.1: string-width "^4.2.3" strip-ansi "^6.0.1" -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: +tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== + +tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== @@ -6602,24 +6849,23 @@ tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -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== +terser-webpack-plugin@5.3.17, terser-webpack-plugin@^5.3.16: + version "5.3.17" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz#75ea98876297fbb190d2fbb395e982582b859a67" + integrity sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw== dependencies: - "@jridgewell/trace-mapping" "^0.3.20" + "@jridgewell/trace-mapping" "^0.3.25" jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.26.0" + schema-utils "^4.3.0" + terser "^5.31.1" -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== +terser@^5.10.0, terser@^5.31.1: + version "5.46.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695" + integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg== dependencies: "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" + acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" @@ -6741,16 +6987,11 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.3.0: - 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" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -6864,6 +7105,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" @@ -6985,10 +7231,10 @@ 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== -watchpack@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== +watchpack@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.1.tgz#dd38b601f669e0cbf567cb802e75cead82cde102" + integrity sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -7036,39 +7282,41 @@ webpack-merge@^5.7.3: flat "^5.0.2" wildcard "^2.0.0" -webpack-sources@^3.2.3: - version "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-sources@^3.3.3: + version "3.3.4" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891" + integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q== -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== +webpack@5.105.2: + version "5.105.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.2.tgz#f3b76f9fc36f1152e156e63ffda3bbb82e6739ea" + integrity sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw== dependencies: - "@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-attributes "^1.9.5" - browserslist "^4.21.10" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.15.0" + acorn-import-phases "^1.0.3" + browserslist "^4.28.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.1" - es-module-lexer "^1.2.1" + enhanced-resolve "^5.19.0" + es-module-lexer "^2.0.0" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" + loader-runner "^4.3.1" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.2.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.3.10" - watchpack "^2.4.1" - webpack-sources "^3.2.3" + schema-utils "^4.3.3" + tapable "^2.3.0" + terser-webpack-plugin "^5.3.16" + watchpack "^2.5.1" + webpack-sources "^3.3.3" websocket-driver@>=0.5.1: version "0.7.4" @@ -7192,13 +7440,6 @@ ws@^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" - resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" - integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== - dependencies: - cuint "^0.2.2" - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"