mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-16 21:15:28 -04:00
Compare commits
2 Commits
sidebar-cl
...
v5-queue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f359e975d | ||
|
|
e213f156af |
2
.github/actions/build/action.yml
vendored
2
.github/actions/build/action.yml
vendored
@@ -21,7 +21,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
- name: Setup Environment Variables
|
- name: Setup Environment Variables
|
||||||
id: variables
|
id: variables
|
||||||
|
|||||||
14
.github/actions/test/action.yml
vendored
14
.github/actions/test/action.yml
vendored
@@ -4,8 +4,6 @@ description: Runs unit/integration tests
|
|||||||
inputs:
|
inputs:
|
||||||
use_postgres:
|
use_postgres:
|
||||||
description: 'Whether postgres should be used for the database'
|
description: 'Whether postgres should be used for the database'
|
||||||
postgres-version:
|
|
||||||
description: 'Which postgres version should be used for the database'
|
|
||||||
os:
|
os:
|
||||||
description: 'OS that the tests are running on'
|
description: 'OS that the tests are running on'
|
||||||
required: true
|
required: true
|
||||||
@@ -29,18 +27,16 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
- name: Setup Postgres
|
- name: Setup Postgres
|
||||||
if: ${{ inputs.use_postgres }}
|
if: ${{ inputs.use_postgres }}
|
||||||
uses: ikalnytskyi/action-setup-postgres@v7
|
uses: ikalnytskyi/action-setup-postgres@v4
|
||||||
with:
|
|
||||||
postgres-version: ${{ inputs.postgres-version }}
|
|
||||||
|
|
||||||
- name: Setup Test Variables
|
- name: Setup Test Variables
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "RESULTS_NAME=${{ inputs.integration_tests && 'integation-' || 'unit-' }}${{ inputs.artifact }}${{ inputs.use_postgres && '-postgres' }}${{ inputs.use_postgres && inputs.postgres-version && inputs.postgres-version }}" >> "$GITHUB_ENV"
|
echo "RESULTS_NAME=${{ inputs.integration_tests && 'integation-' || 'unit-' }}${{ inputs.artifact }}${{ inputs.use_postgres && '-postgres' }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Setup Postgres Environment Variables
|
- name: Setup Postgres Environment Variables
|
||||||
if: ${{ inputs.use_postgres }}
|
if: ${{ inputs.use_postgres }}
|
||||||
@@ -52,14 +48,14 @@ runs:
|
|||||||
echo "Sonarr__Postgres__Password=postgres" >> "$GITHUB_ENV"
|
echo "Sonarr__Postgres__Password=postgres" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Download Artifact
|
- name: Download Artifact
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.artifact }}
|
name: ${{ inputs.artifact }}
|
||||||
path: _tests
|
path: _tests
|
||||||
|
|
||||||
- name: Download Binary Artifact
|
- name: Download Binary Artifact
|
||||||
if: ${{ inputs.integration_tests }}
|
if: ${{ inputs.integration_tests }}
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.binary_artifact }}
|
name: ${{ inputs.binary_artifact }}
|
||||||
path: _output
|
path: _output
|
||||||
|
|||||||
15
.github/workflows/build_v5.yml
vendored
15
.github/workflows/build_v5.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: ./.github/actions/build
|
uses: ./.github/actions/build
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Volta
|
- name: Volta
|
||||||
uses: volta-cli/action@v4
|
uses: volta-cli/action@v4
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
@@ -152,13 +152,9 @@ jobs:
|
|||||||
unit_test_postgres:
|
unit_test_postgres:
|
||||||
needs: backend
|
needs: backend
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
postgres-version: [16, 17]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
@@ -168,7 +164,6 @@ jobs:
|
|||||||
pattern: Sonarr.*.Test.dll
|
pattern: Sonarr.*.Test.dll
|
||||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
use_postgres: true
|
use_postgres: true
|
||||||
postgres-version: ${{ matrix.postgres-version }}
|
|
||||||
|
|
||||||
integration_test:
|
integration_test:
|
||||||
needs: [prepare, backend]
|
needs: [prepare, backend]
|
||||||
@@ -195,7 +190,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { setQueueOptions } from 'Activity/Queue/queueOptionsStore';
|
|
||||||
import { SelectProvider } from 'App/SelectContext';
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -16,8 +16,20 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
clearBlocklist,
|
||||||
|
fetchBlocklist,
|
||||||
|
gotoBlocklistPage,
|
||||||
|
removeBlocklistItems,
|
||||||
|
setBlocklistFilter,
|
||||||
|
setBlocklistSort,
|
||||||
|
setBlocklistTableOption,
|
||||||
|
} from 'Store/Actions/blocklistActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
@@ -31,35 +43,27 @@ import {
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||||
import {
|
|
||||||
setBlocklistOption,
|
|
||||||
useBlocklistOptions,
|
|
||||||
} from './blocklistOptionsStore';
|
|
||||||
import BlocklistRow from './BlocklistRow';
|
import BlocklistRow from './BlocklistRow';
|
||||||
import useBlocklist, {
|
|
||||||
useFilters,
|
|
||||||
useRemoveBlocklistItems,
|
|
||||||
} from './useBlocklist';
|
|
||||||
|
|
||||||
function Blocklist() {
|
function Blocklist() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
records,
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isFetching,
|
isRemoving,
|
||||||
isFetched,
|
} = useSelector((state: AppState) => state.blocklist);
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
page,
|
|
||||||
goToPage,
|
|
||||||
refetch,
|
|
||||||
} = useBlocklist();
|
|
||||||
|
|
||||||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
|
||||||
useBlocklistOptions();
|
|
||||||
|
|
||||||
const filters = useFilters();
|
|
||||||
const { isRemoving, removeBlocklistItems } = useRemoveBlocklistItems();
|
|
||||||
|
|
||||||
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
||||||
const isClearingBlocklistExecuting = useSelector(
|
const isClearingBlocklistExecuting = useSelector(
|
||||||
@@ -78,27 +82,28 @@ function Blocklist() {
|
|||||||
return getSelectedIds(selectedState);
|
return getSelectedIds(selectedState);
|
||||||
}, [selectedState]);
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const wasClearingBlocklistExecuting = usePrevious(
|
||||||
|
isClearingBlocklistExecuting
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectAllChange = useCallback(
|
const handleSelectAllChange = useCallback(
|
||||||
({ value }: CheckInputChanged) => {
|
({ value }: CheckInputChanged) => {
|
||||||
setSelectState({
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
type: value ? 'selectAll' : 'unselectAll',
|
|
||||||
items: records,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[records, setSelectState]
|
[items, setSelectState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectedChange = useCallback(
|
const handleSelectedChange = useCallback(
|
||||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
setSelectState({
|
setSelectState({
|
||||||
type: 'toggleSelected',
|
type: 'toggleSelected',
|
||||||
items: records,
|
items,
|
||||||
id,
|
id,
|
||||||
isSelected: value,
|
isSelected: value,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[records, setSelectState]
|
[items, setSelectState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveSelectedPress = useCallback(() => {
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
@@ -106,9 +111,9 @@ function Blocklist() {
|
|||||||
}, [setIsConfirmRemoveModalOpen]);
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
const handleRemoveSelectedConfirmed = useCallback(() => {
|
const handleRemoveSelectedConfirmed = useCallback(() => {
|
||||||
removeBlocklistItems({ ids: selectedIds });
|
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
||||||
setIsConfirmRemoveModalOpen(false);
|
setIsConfirmRemoveModalOpen(false);
|
||||||
}, [selectedIds, setIsConfirmRemoveModalOpen, removeBlocklistItems]);
|
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
||||||
|
|
||||||
const handleConfirmRemoveModalClose = useCallback(() => {
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
setIsConfirmRemoveModalOpen(false);
|
setIsConfirmRemoveModalOpen(false);
|
||||||
@@ -119,46 +124,66 @@ function Blocklist() {
|
|||||||
}, [setIsConfirmClearModalOpen]);
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
const handleClearBlocklistConfirmed = useCallback(() => {
|
const handleClearBlocklistConfirmed = useCallback(() => {
|
||||||
dispatch(
|
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
||||||
executeCommand({
|
|
||||||
name: commandNames.CLEAR_BLOCKLIST,
|
|
||||||
commandFinished: () => {
|
|
||||||
goToPage(1);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setIsConfirmClearModalOpen(false);
|
setIsConfirmClearModalOpen(false);
|
||||||
}, [setIsConfirmClearModalOpen, goToPage, dispatch]);
|
}, [setIsConfirmClearModalOpen, dispatch]);
|
||||||
|
|
||||||
const handleConfirmClearModalClose = useCallback(() => {
|
const handleConfirmClearModalClose = useCallback(() => {
|
||||||
setIsConfirmClearModalOpen(false);
|
setIsConfirmClearModalOpen(false);
|
||||||
}, [setIsConfirmClearModalOpen]);
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoBlocklistPage,
|
||||||
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string | number) => {
|
(selectedFilterKey: string | number) => {
|
||||||
setBlocklistOption('selectedFilterKey', selectedFilterKey);
|
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSortPress = useCallback((sortKey: string) => {
|
const handleSortPress = useCallback(
|
||||||
setBlocklistOption('sortKey', sortKey);
|
(sortKey: string) => {
|
||||||
}, []);
|
dispatch(setBlocklistSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const handleTableOptionChange = useCallback(
|
const handleTableOptionChange = useCallback(
|
||||||
(payload: TableOptionsChangePayload) => {
|
(payload: TableOptionsChangePayload) => {
|
||||||
setQueueOptions(payload);
|
dispatch(setBlocklistTableOption(payload));
|
||||||
|
|
||||||
if (payload.pageSize) {
|
if (payload.pageSize) {
|
||||||
goToPage(1);
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[goToPage]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearBlocklist());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const repopulate = () => {
|
const repopulate = () => {
|
||||||
refetch();
|
dispatch(fetchBlocklist());
|
||||||
};
|
};
|
||||||
|
|
||||||
registerPagePopulator(repopulate);
|
registerPagePopulator(repopulate);
|
||||||
@@ -166,10 +191,16 @@ function Blocklist() {
|
|||||||
return () => {
|
return () => {
|
||||||
unregisterPagePopulator(repopulate);
|
unregisterPagePopulator(repopulate);
|
||||||
};
|
};
|
||||||
}, [refetch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectProvider items={records}>
|
<SelectProvider items={items}>
|
||||||
<PageContent title={translate('Blocklist')}>
|
<PageContent title={translate('Blocklist')}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
@@ -184,7 +215,7 @@ function Blocklist() {
|
|||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('Clear')}
|
label={translate('Clear')}
|
||||||
iconName={icons.CLEAR}
|
iconName={icons.CLEAR}
|
||||||
isDisabled={!records.length}
|
isDisabled={!items.length}
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
onPress={handleClearBlocklistPress}
|
onPress={handleClearBlocklistPress}
|
||||||
/>
|
/>
|
||||||
@@ -214,13 +245,13 @@ function Blocklist() {
|
|||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
{isLoading && !isFetched ? <LoadingIndicator /> : null}
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
{!isLoading && !!error ? (
|
{!isFetching && !!error ? (
|
||||||
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isFetched && !error && !records.length ? (
|
{isPopulated && !error && !items.length ? (
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{selectedFilterKey === 'all'
|
{selectedFilterKey === 'all'
|
||||||
? translate('NoBlocklistItems')
|
? translate('NoBlocklistItems')
|
||||||
@@ -228,7 +259,7 @@ function Blocklist() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isFetched && !error && !!records.length ? (
|
{isPopulated && !error && !!items.length ? (
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
selectAll={true}
|
selectAll={true}
|
||||||
@@ -243,7 +274,7 @@ function Blocklist() {
|
|||||||
onSortPress={handleSortPress}
|
onSortPress={handleSortPress}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{records.map((item) => {
|
{items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<BlocklistRow
|
<BlocklistRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -261,7 +292,11 @@ function Blocklist() {
|
|||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
totalRecords={totalRecords}
|
totalRecords={totalRecords}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
onPageSelect={goToPage}
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -16,19 +16,13 @@ interface BlocklistDetailsModalProps {
|
|||||||
protocol: DownloadProtocol;
|
protocol: DownloadProtocol;
|
||||||
indexer?: string;
|
indexer?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
source?: string;
|
|
||||||
onModalClose: () => void;
|
onModalClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlocklistDetailsModal({
|
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
||||||
isOpen,
|
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
||||||
sourceTitle,
|
props;
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
source,
|
|
||||||
onModalClose,
|
|
||||||
}: BlocklistDetailsModalProps) {
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
@@ -56,9 +50,6 @@ function BlocklistDetailsModal({
|
|||||||
data={message}
|
data={message}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{source ? (
|
|
||||||
<DescriptionListItem title={translate('Source')} data={source} />
|
|
||||||
) : null}
|
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,50 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||||
import { setBlocklistOption } from './blocklistOptionsStore';
|
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||||
import useBlocklist, { FILTER_BUILDER } from './useBlocklist';
|
|
||||||
|
function createBlocklistSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.items,
|
||||||
|
(blocklistItems) => {
|
||||||
|
return blocklistItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type BlocklistFilterModalProps = FilterModalProps<History>;
|
type BlocklistFilterModalProps = FilterModalProps<History>;
|
||||||
|
|
||||||
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
const { records } = useBlocklist();
|
const sectionItems = useSelector(createBlocklistSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'blocklist';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
const dispatchSetFilter = useCallback(
|
||||||
({ selectedFilterKey }: { selectedFilterKey: string | number }) => {
|
(payload: unknown) => {
|
||||||
setBlocklistOption('selectedFilterKey', selectedFilterKey);
|
dispatch(setBlocklistFilter(payload));
|
||||||
},
|
},
|
||||||
[]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={records}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={FILTER_BUILDER}
|
filterBuilderProps={filterBuilderProps}
|
||||||
customFilterType="blocklist"
|
customFilterType={customFilterType}
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
@@ -11,11 +12,11 @@ import EpisodeQuality from 'Episode/EpisodeQuality';
|
|||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||||
import Blocklist from 'typings/Blocklist';
|
import Blocklist from 'typings/Blocklist';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||||
import { useRemoveBlocklistItem } from './useBlocklist';
|
|
||||||
import styles from './BlocklistRow.css';
|
import styles from './BlocklistRow.css';
|
||||||
|
|
||||||
interface BlocklistRowProps extends Blocklist {
|
interface BlocklistRowProps extends Blocklist {
|
||||||
@@ -24,24 +25,25 @@ interface BlocklistRowProps extends Blocklist {
|
|||||||
onSelectedChange: (options: SelectStateInputProps) => void;
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlocklistRow({
|
function BlocklistRow(props: BlocklistRowProps) {
|
||||||
id,
|
const {
|
||||||
seriesId,
|
id,
|
||||||
sourceTitle,
|
seriesId,
|
||||||
languages,
|
sourceTitle,
|
||||||
quality,
|
languages,
|
||||||
customFormats,
|
quality,
|
||||||
date,
|
customFormats,
|
||||||
protocol,
|
date,
|
||||||
indexer,
|
protocol,
|
||||||
message,
|
indexer,
|
||||||
source,
|
message,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
}: BlocklistRowProps) {
|
} = props;
|
||||||
|
|
||||||
const series = useSeries(seriesId);
|
const series = useSeries(seriesId);
|
||||||
const { isRemoving, removeBlocklistItem } = useRemoveBlocklistItem(id);
|
const dispatch = useDispatch();
|
||||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleDetailsPress = useCallback(() => {
|
const handleDetailsPress = useCallback(() => {
|
||||||
@@ -53,8 +55,8 @@ function BlocklistRow({
|
|||||||
}, [setIsDetailsModalOpen]);
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
const handleRemovePress = useCallback(() => {
|
const handleRemovePress = useCallback(() => {
|
||||||
removeBlocklistItem();
|
dispatch(removeBlocklistItem({ id }));
|
||||||
}, [removeBlocklistItem]);
|
}, [id, dispatch]);
|
||||||
|
|
||||||
if (!series) {
|
if (!series) {
|
||||||
return null;
|
return null;
|
||||||
@@ -137,7 +139,6 @@ function BlocklistRow({
|
|||||||
title={translate('RemoveFromBlocklist')}
|
title={translate('RemoveFromBlocklist')}
|
||||||
name={icons.REMOVE}
|
name={icons.REMOVE}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={handleRemovePress}
|
onPress={handleRemovePress}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
@@ -153,7 +154,6 @@ function BlocklistRow({
|
|||||||
protocol={protocol}
|
protocol={protocol}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
message={message}
|
message={message}
|
||||||
source={source}
|
|
||||||
onModalClose={handleDetailsModalClose}
|
onModalClose={handleDetailsModalClose}
|
||||||
/>
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import {
|
|
||||||
createOptionsStore,
|
|
||||||
PageableOptions,
|
|
||||||
} from 'Helpers/Hooks/useOptionsStore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
export type BlocklistOptions = PageableOptions;
|
|
||||||
|
|
||||||
const { useOptions, useOption, setOptions, setOption } =
|
|
||||||
createOptionsStore<BlocklistOptions>('blocklist_options', () => {
|
|
||||||
return {
|
|
||||||
pageSize: 20,
|
|
||||||
selectedFilterKey: 'all',
|
|
||||||
sortKey: 'time',
|
|
||||||
sortDirection: 'descending',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'series.sortTitle',
|
|
||||||
label: () => translate('SeriesTitle'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sourceTitle',
|
|
||||||
label: () => translate('SourceTitle'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'languages',
|
|
||||||
label: () => translate('Languages'),
|
|
||||||
isVisible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'quality',
|
|
||||||
label: () => translate('Quality'),
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'customFormats',
|
|
||||||
label: () => translate('Formats'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
label: () => translate('Date'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'indexer',
|
|
||||||
label: () => translate('Indexer'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: '',
|
|
||||||
columnLabel: () => translate('Actions'),
|
|
||||||
isVisible: true,
|
|
||||||
isModifiable: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useBlocklistOptions = useOptions;
|
|
||||||
export const setBlocklistOptions = setOptions;
|
|
||||||
export const useBlocklistOption = useOption;
|
|
||||||
export const setBlocklistOption = setOption;
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
|
|
||||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
|
||||||
import usePage from 'Helpers/Hooks/usePage';
|
|
||||||
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
|
|
||||||
import { filterBuilderValueTypes } from 'Helpers/Props';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import Blocklist from 'typings/Blocklist';
|
|
||||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import { useBlocklistOptions } from './blocklistOptionsStore';
|
|
||||||
|
|
||||||
interface BulkBlocklistData {
|
|
||||||
ids: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FILTERS: Filter[] = [
|
|
||||||
{
|
|
||||||
key: 'all',
|
|
||||||
label: () => translate('All'),
|
|
||||||
filters: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const FILTER_BUILDER: FilterBuilderProp<Blocklist>[] = [
|
|
||||||
{
|
|
||||||
name: 'seriesIds',
|
|
||||||
label: () => translate('Series'),
|
|
||||||
type: 'equal',
|
|
||||||
valueType: filterBuilderValueTypes.SERIES,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'protocols',
|
|
||||||
label: () => translate('Protocol'),
|
|
||||||
type: 'equal',
|
|
||||||
valueType: filterBuilderValueTypes.PROTOCOL,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const useBlocklist = () => {
|
|
||||||
const { page, goToPage } = usePage('blocklist');
|
|
||||||
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
|
|
||||||
useBlocklistOptions();
|
|
||||||
const customFilters = useSelector(
|
|
||||||
createCustomFiltersSelector('blocklist')
|
|
||||||
) as CustomFilter[];
|
|
||||||
|
|
||||||
const filters = useMemo(() => {
|
|
||||||
return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);
|
|
||||||
}, [selectedFilterKey, customFilters]);
|
|
||||||
|
|
||||||
const { refetch, ...query } = usePagedApiQuery<Blocklist>({
|
|
||||||
path: '/blocklist',
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
filters,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
queryOptions: {
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
goToPage,
|
|
||||||
page,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useBlocklist;
|
|
||||||
|
|
||||||
export const useFilters = () => {
|
|
||||||
return FILTERS;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRemoveBlocklistItem = (id: number) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { mutate, isPending } = useApiMutation<unknown, void>({
|
|
||||||
path: `/blocklist/${id}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/blocklist'] });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
removeBlocklistItem: mutate,
|
|
||||||
isRemoving: isPending,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRemoveBlocklistItems = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { mutate, isPending } = useApiMutation<unknown, BulkBlocklistData>({
|
|
||||||
path: `/blocklist/bulk`,
|
|
||||||
method: 'DELETE',
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/blocklist'] });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
removeBlocklistItems: mutate,
|
|
||||||
isRemoving: isPending,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -174,7 +174,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const { indexer, message, source } = data as DownloadFailedHistory;
|
const { message, indexer } = data as DownloadFailedHistory;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
@@ -195,10 +195,6 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
|||||||
{message ? (
|
{message ? (
|
||||||
<DescriptionListItem title={translate('Message')} data={message} />
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{source ? (
|
|
||||||
<DescriptionListItem title={translate('Source')} data={source} />
|
|
||||||
) : null}
|
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,13 +54,17 @@ import useQueue, {
|
|||||||
useRemoveQueueItems,
|
useRemoveQueueItems,
|
||||||
} from './useQueue';
|
} from './useQueue';
|
||||||
|
|
||||||
|
const DEFAULT_DATA = {
|
||||||
|
records: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0,
|
||||||
|
};
|
||||||
|
|
||||||
function Queue() {
|
function Queue() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
records,
|
data,
|
||||||
totalPages,
|
|
||||||
totalRecords,
|
|
||||||
error,
|
error,
|
||||||
isFetching,
|
isFetching,
|
||||||
isFetched,
|
isFetched,
|
||||||
@@ -70,6 +74,8 @@ function Queue() {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useQueue();
|
} = useQueue();
|
||||||
|
|
||||||
|
const { records, totalPages = 0, totalRecords } = data ?? DEFAULT_DATA;
|
||||||
|
|
||||||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
||||||
useQueueOptions();
|
useQueueOptions();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import useQueue, { FILTER_BUILDER } from './useQueue';
|
|||||||
type QueueFilterModalProps = FilterModalProps<History>;
|
type QueueFilterModalProps = FilterModalProps<History>;
|
||||||
|
|
||||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
const { records } = useQueue();
|
const { data } = useQueue();
|
||||||
|
const customFilterType = 'queue';
|
||||||
|
|
||||||
const dispatchSetFilter = useCallback(
|
const dispatchSetFilter = useCallback(
|
||||||
({ selectedFilterKey }: { selectedFilterKey: string | number }) => {
|
({ selectedFilterKey }: { selectedFilterKey: string | number }) => {
|
||||||
@@ -18,9 +19,9 @@ export default function QueueFilterModal(props: QueueFilterModalProps) {
|
|||||||
return (
|
return (
|
||||||
<FilterModal
|
<FilterModal
|
||||||
{...props}
|
{...props}
|
||||||
sectionItems={records}
|
sectionItems={data?.records ?? []}
|
||||||
filterBuilderProps={FILTER_BUILDER}
|
filterBuilderProps={FILTER_BUILDER}
|
||||||
customFilterType="queue"
|
customFilterType={customFilterType}
|
||||||
dispatchSetFilter={dispatchSetFilter}
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ` - ${translate('Importing')}`;
|
||||||
iconKind = kinds.PRIMARY;
|
iconKind = kinds.PURPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import {
|
import Column from 'Components/Table/Column';
|
||||||
createOptionsStore,
|
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||||
PageableOptions,
|
|
||||||
} from 'Helpers/Hooks/useOptionsStore';
|
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
interface QueueRemovalOptions {
|
interface QueueRemovalOptions {
|
||||||
@@ -12,8 +11,13 @@ interface QueueRemovalOptions {
|
|||||||
blocklistMethod: 'blocklistAndSearch' | 'blocklistOnly' | 'doNotBlocklist';
|
blocklistMethod: 'blocklistAndSearch' | 'blocklistOnly' | 'doNotBlocklist';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueOptions extends PageableOptions {
|
export interface QueueOptions {
|
||||||
includeUnknownSeriesItems: boolean;
|
includeUnknownSeriesItems: boolean;
|
||||||
|
pageSize: number;
|
||||||
|
selectedFilterKey: string | number;
|
||||||
|
sortKey: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
columns: Column[];
|
||||||
removalOptions: QueueRemovalOptions;
|
removalOptions: QueueRemovalOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
|
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
|
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
|
||||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||||
@@ -90,9 +90,16 @@ const useQueue = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleGoToPage = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
goToPage(page);
|
||||||
|
},
|
||||||
|
[goToPage]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...query,
|
...query,
|
||||||
goToPage,
|
goToPage: handleGoToPage,
|
||||||
page,
|
page,
|
||||||
refetch,
|
refetch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface TagDetail extends ModelBase {
|
|||||||
indexerIds: number[];
|
indexerIds: number[];
|
||||||
notificationIds: number[];
|
notificationIds: number[];
|
||||||
restrictionIds: number[];
|
restrictionIds: number[];
|
||||||
excludedReleaseProfileIds: number[];
|
|
||||||
seriesIds: number[];
|
seriesIds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import NumberInput, { NumberInputChanged } from './NumberInput';
|
|
||||||
|
|
||||||
export interface FloatInputProps {
|
|
||||||
name: string;
|
|
||||||
value?: number | null;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
onChange: (change: NumberInputChanged) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FloatInput(props: FloatInputProps) {
|
|
||||||
return <NumberInput {...props} isFloat={true} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FloatInput;
|
|
||||||
@@ -7,7 +7,6 @@ import translate from 'Utilities/String/translate';
|
|||||||
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
|
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
|
||||||
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
|
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
|
||||||
import CheckInput, { CheckInputProps } from './CheckInput';
|
import CheckInput, { CheckInputProps } from './CheckInput';
|
||||||
import FloatInput, { FloatInputProps } from './FloatInput';
|
|
||||||
import { FormInputButtonProps } from './FormInputButton';
|
import { FormInputButtonProps } from './FormInputButton';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
|
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
|
||||||
@@ -66,7 +65,7 @@ const componentMap: Record<InputType, ElementType> = {
|
|||||||
downloadClientSelect: DownloadClientSelectInput,
|
downloadClientSelect: DownloadClientSelectInput,
|
||||||
dynamicSelect: ProviderDataSelectInput,
|
dynamicSelect: ProviderDataSelectInput,
|
||||||
file: TextInput,
|
file: TextInput,
|
||||||
float: FloatInput,
|
float: NumberInput,
|
||||||
indexerFlagsSelect: IndexerFlagsSelectInput,
|
indexerFlagsSelect: IndexerFlagsSelectInput,
|
||||||
indexerSelect: IndexerSelectInput,
|
indexerSelect: IndexerSelectInput,
|
||||||
keyValueList: KeyValueListInput,
|
keyValueList: KeyValueListInput,
|
||||||
@@ -111,7 +110,7 @@ type PickProps<V, C extends InputType> = C extends 'text'
|
|||||||
: C extends 'file'
|
: C extends 'file'
|
||||||
? TextInputProps
|
? TextInputProps
|
||||||
: C extends 'float'
|
: C extends 'float'
|
||||||
? FloatInputProps
|
? TextInputProps
|
||||||
: C extends 'indexerFlagsSelect'
|
: C extends 'indexerFlagsSelect'
|
||||||
? IndexerFlagsSelectInputProps
|
? IndexerFlagsSelectInputProps
|
||||||
: C extends 'indexerSelect'
|
: C extends 'indexerSelect'
|
||||||
|
|||||||
@@ -24,17 +24,13 @@ function parseValue(
|
|||||||
return newValue;
|
return newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberInputChanged extends InputChanged<number | null> {
|
|
||||||
isFloat?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NumberInputProps
|
export interface NumberInputProps
|
||||||
extends Omit<TextInputProps, 'value' | 'onChange'> {
|
extends Omit<TextInputProps, 'value' | 'onChange'> {
|
||||||
value?: number | null;
|
value?: number | null;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
isFloat?: boolean;
|
isFloat?: boolean;
|
||||||
onChange: (change: NumberInputChanged) => void;
|
onChange: (input: InputChanged<number | null>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NumberInput({
|
function NumberInput({
|
||||||
@@ -54,14 +50,11 @@ function NumberInput({
|
|||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
({ name, value: newValue }: InputChanged<string>) => {
|
({ name, value: newValue }: InputChanged<string>) => {
|
||||||
const parsedValue = parseValue(newValue, isFloat, min, max);
|
setValue(newValue);
|
||||||
|
|
||||||
setValue(parsedValue == null ? '' : parsedValue.toString());
|
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
name,
|
name,
|
||||||
value: parsedValue,
|
value: parseValue(newValue, isFloat, min, max),
|
||||||
isFloat,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[isFloat, min, max, onChange, setValue]
|
[isFloat, min, max, onChange, setValue]
|
||||||
@@ -82,7 +75,6 @@ function NumberInput({
|
|||||||
onChange({
|
onChange({
|
||||||
name,
|
name,
|
||||||
value: parsedValue,
|
value: parsedValue,
|
||||||
isFloat,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
isFocused.current = false;
|
isFocused.current = false;
|
||||||
|
|||||||
@@ -5,18 +5,14 @@ import { addTag } from 'Store/Actions/tagActions';
|
|||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import { InputChanged } from 'typings/inputs';
|
import { InputChanged } from 'typings/inputs';
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import TagInput, { TagBase, TagInputProps } from './TagInput';
|
import TagInput, { TagBase } from './TagInput';
|
||||||
|
|
||||||
interface SeriesTag extends TagBase {
|
interface SeriesTag extends TagBase {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeriesTagInputProps<V>
|
export interface SeriesTagInputProps<V> {
|
||||||
extends Omit<
|
|
||||||
TagInputProps<SeriesTag>,
|
|
||||||
'tags' | 'tagList' | 'onTagAdd' | 'onTagDelete' | 'onChange'
|
|
||||||
> {
|
|
||||||
name: string;
|
name: string;
|
||||||
value: V;
|
value: V;
|
||||||
onChange: (change: InputChanged<V>) => void;
|
onChange: (change: InputChanged<V>) => void;
|
||||||
@@ -67,7 +63,6 @@ export default function SeriesTagInput<V extends number | number[]>({
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
...otherProps
|
|
||||||
}: SeriesTagInputProps<V>) {
|
}: SeriesTagInputProps<V>) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isArray = Array.isArray(value);
|
const isArray = Array.isArray(value);
|
||||||
@@ -140,7 +135,6 @@ export default function SeriesTagInput<V extends number | number[]>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TagInput
|
<TagInput
|
||||||
{...otherProps}
|
|
||||||
name={name}
|
name={name}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
tagList={tagList}
|
tagList={tagList}
|
||||||
|
|||||||
@@ -26,10 +26,6 @@
|
|||||||
color: var(--warningColor);
|
color: var(--warningColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
|
||||||
color: var(--primaryColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
color: var(--purple);
|
color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/src/Components/Icon.css.d.ts
vendored
1
frontend/src/Components/Icon.css.d.ts
vendored
@@ -6,7 +6,6 @@ interface CssExports {
|
|||||||
'disabled': string;
|
'disabled': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'pink': string;
|
'pink': string;
|
||||||
'primary': string;
|
|
||||||
'purple': string;
|
'purple': string;
|
||||||
'success': string;
|
'success': string;
|
||||||
'warning': string;
|
'warning': string;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
.modal {
|
.modal {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 90%;
|
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.header {
|
.header {
|
||||||
|
z-index: 3;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -7,40 +7,6 @@
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: $headerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoLink {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebarCloseButton {
|
|
||||||
composes: button from '~Components/Link/IconButton.css';
|
|
||||||
|
|
||||||
margin-right: 15px;
|
|
||||||
color: #e1e2e3;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--sonarrBlue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'logo': string;
|
|
||||||
'logoContainer': string;
|
|
||||||
'logoLink': string;
|
|
||||||
'sidebar': string;
|
'sidebar': string;
|
||||||
'sidebarCloseButton': string;
|
|
||||||
'sidebarContainer': string;
|
'sidebarContainer': string;
|
||||||
'sidebarHeader': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -10,8 +11,6 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
||||||
import { IconName } from 'Components/Icon';
|
import { IconName } from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
@@ -231,6 +230,10 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
transition: 'none',
|
transition: 'none',
|
||||||
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
|
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
|
||||||
});
|
});
|
||||||
|
const [sidebarStyle, setSidebarStyle] = useState({
|
||||||
|
top: dimensions.headerHeight,
|
||||||
|
height: `${window.innerHeight - HEADER_HEIGHT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
const urlBase = window.Sonarr.urlBase;
|
const urlBase = window.Sonarr.urlBase;
|
||||||
const pathname = urlBase
|
const pathname = urlBase
|
||||||
@@ -296,6 +299,22 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleWindowScroll = useCallback(() => {
|
||||||
|
const windowScroll =
|
||||||
|
window.scrollY == null
|
||||||
|
? document.documentElement.scrollTop
|
||||||
|
: window.scrollY;
|
||||||
|
const sidebarTop = Math.max(HEADER_HEIGHT - windowScroll, 0);
|
||||||
|
const sidebarHeight = window.innerHeight - sidebarTop;
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
setSidebarStyle({
|
||||||
|
top: `${sidebarTop}px`,
|
||||||
|
height: `${sidebarHeight}px`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isSmallScreen]);
|
||||||
|
|
||||||
const handleTouchStart = useCallback(
|
const handleTouchStart = useCallback(
|
||||||
(event: TouchEvent) => {
|
(event: TouchEvent) => {
|
||||||
const touches = event.touches;
|
const touches = event.touches;
|
||||||
@@ -377,13 +396,10 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
touchStartY.current = null;
|
touchStartY.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSidebarClosePress = useCallback(() => {
|
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
window.addEventListener('click', handleWindowClick, { capture: true });
|
window.addEventListener('click', handleWindowClick, { capture: true });
|
||||||
|
window.addEventListener('scroll', handleWindowScroll);
|
||||||
window.addEventListener('touchstart', handleTouchStart);
|
window.addEventListener('touchstart', handleTouchStart);
|
||||||
window.addEventListener('touchmove', handleTouchMove);
|
window.addEventListener('touchmove', handleTouchMove);
|
||||||
window.addEventListener('touchend', handleTouchEnd);
|
window.addEventListener('touchend', handleTouchEnd);
|
||||||
@@ -392,6 +408,7 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('click', handleWindowClick, { capture: true });
|
window.removeEventListener('click', handleWindowClick, { capture: true });
|
||||||
|
window.removeEventListener('scroll', handleWindowScroll);
|
||||||
window.removeEventListener('touchstart', handleTouchStart);
|
window.removeEventListener('touchstart', handleTouchStart);
|
||||||
window.removeEventListener('touchmove', handleTouchMove);
|
window.removeEventListener('touchmove', handleTouchMove);
|
||||||
window.removeEventListener('touchend', handleTouchEnd);
|
window.removeEventListener('touchend', handleTouchEnd);
|
||||||
@@ -400,6 +417,7 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
}, [
|
}, [
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
handleWindowClick,
|
handleWindowClick,
|
||||||
|
handleWindowScroll,
|
||||||
handleTouchStart,
|
handleTouchStart,
|
||||||
handleTouchMove,
|
handleTouchMove,
|
||||||
handleTouchEnd,
|
handleTouchEnd,
|
||||||
@@ -438,37 +456,13 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className={styles.sidebarContainer}
|
className={classNames(styles.sidebarContainer)}
|
||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
>
|
>
|
||||||
{isSmallScreen ? (
|
|
||||||
<div className={styles.sidebarHeader}>
|
|
||||||
<div className={styles.logoContainer}>
|
|
||||||
<Link className={styles.logoLink} to="/">
|
|
||||||
<img
|
|
||||||
className={styles.logo}
|
|
||||||
src={`${window.Sonarr.urlBase}/Content/Images/logo.svg`}
|
|
||||||
alt="Sonarr Logo"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
className={styles.sidebarCloseButton}
|
|
||||||
name={icons.CLOSE}
|
|
||||||
aria-label={translate('Close')}
|
|
||||||
size={20}
|
|
||||||
onPress={handleSidebarClosePress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ScrollerComponent
|
<ScrollerComponent
|
||||||
className={styles.sidebar}
|
className={styles.sidebar}
|
||||||
scrollDirection="vertical"
|
scrollDirection="vertical"
|
||||||
style={{
|
style={sidebarStyle}
|
||||||
height: `${window.innerHeight - HEADER_HEIGHT}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{LINKS.map((link) => {
|
{LINKS.map((link) => {
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tag } from 'App/State/TagsAppState';
|
import { Tag } from 'App/State/TagsAppState';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import { Kind } from 'Helpers/Props/kinds';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import Label, { LabelProps } from './Label';
|
import Label from './Label';
|
||||||
import styles from './TagList.css';
|
import styles from './TagList.css';
|
||||||
|
|
||||||
interface TagListProps {
|
interface TagListProps {
|
||||||
tags: number[];
|
tags: number[];
|
||||||
tagList: Tag[];
|
tagList: Tag[];
|
||||||
kind?: Extract<Kind, LabelProps['kind']>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TagList({
|
function TagList({ tags, tagList }: TagListProps) {
|
||||||
tags,
|
|
||||||
tagList,
|
|
||||||
kind = kinds.INFO,
|
|
||||||
}: TagListProps) {
|
|
||||||
const sortedTags = tags
|
const sortedTags = tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((tag) => !!tag)
|
.filter((tag) => !!tag)
|
||||||
@@ -26,7 +20,7 @@ export default function TagList({
|
|||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
{sortedTags.map((tag) => {
|
{sortedTags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
<Label key={tag.id} kind={kind}>
|
<Label key={tag.id} kind={kinds.INFO}>
|
||||||
{tag.label}
|
{tag.label}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
@@ -34,3 +28,5 @@ export default function TagList({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default TagList;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function AuthenticationRequiredModalContent() {
|
|||||||
dispatch(fetchGeneralSettings());
|
dispatch(fetchGeneralSettings());
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
|
dispatch(clearPendingChanges());
|
||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ function useApiMutation<T, TData>(options: MutationOptions<T, TData>) {
|
|||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
'X-Api-Key': window.Sonarr.apiKey,
|
'X-Api-Key': window.Sonarr.apiKey,
|
||||||
'X-Sonarr-Client': 'Sonarr',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ const useApiQuery = <T>(options: QueryOptions<T>) => {
|
|||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
'X-Api-Key': window.Sonarr.apiKey,
|
'X-Api-Key': window.Sonarr.apiKey,
|
||||||
'X-Sonarr-Client': 'Sonarr',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { StateCreator } from 'zustand';
|
|||||||
import { PersistOptions } from 'zustand/middleware';
|
import { PersistOptions } from 'zustand/middleware';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import { createPersist } from 'Helpers/createPersist';
|
import { createPersist } from 'Helpers/createPersist';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
|
||||||
|
|
||||||
type TSettingsWithoutColumns = object;
|
type TSettingsWithoutColumns = object;
|
||||||
|
|
||||||
@@ -12,14 +11,6 @@ interface TSettingsWithColumns {
|
|||||||
|
|
||||||
type TSettingd = TSettingsWithoutColumns | TSettingsWithColumns;
|
type TSettingd = TSettingsWithoutColumns | TSettingsWithColumns;
|
||||||
|
|
||||||
export interface PageableOptions {
|
|
||||||
pageSize: number;
|
|
||||||
selectedFilterKey: string | number;
|
|
||||||
sortKey: string;
|
|
||||||
sortDirection: SortDirection;
|
|
||||||
columns: Column[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OptionChanged<T> = {
|
export type OptionChanged<T> = {
|
||||||
name: keyof T;
|
name: keyof T;
|
||||||
value: T[keyof T];
|
value: T[keyof T];
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { useHistory } from 'react-router';
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
interface PageStore {
|
interface PageStore {
|
||||||
blocklist: number;
|
|
||||||
events: number;
|
events: number;
|
||||||
queue: number;
|
queue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStore = create<PageStore>(() => ({
|
const pageStore = create<PageStore>(() => ({
|
||||||
blocklist: 1,
|
|
||||||
events: 1,
|
events: 1,
|
||||||
queue: 1,
|
queue: 1,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ interface PagedQueryResponse<T> {
|
|||||||
records: T[];
|
records: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_RECORDS: never[] = [];
|
|
||||||
|
|
||||||
const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
||||||
const { requestOptions, queryKey } = useMemo(() => {
|
const { requestOptions, queryKey } = useMemo(() => {
|
||||||
const {
|
const {
|
||||||
@@ -66,13 +64,12 @@ const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
|||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
'X-Api-Key': window.Sonarr.apiKey,
|
'X-Api-Key': window.Sonarr.apiKey,
|
||||||
'X-Sonarr-Client': 'Sonarr',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
const { data, ...query } = useQuery({
|
return useQuery({
|
||||||
...options.queryOptions,
|
...options.queryOptions,
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
@@ -90,13 +87,6 @@ const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
records: data?.records ?? DEFAULT_RECORDS,
|
|
||||||
totalRecords: data?.totalRecords ?? 0,
|
|
||||||
totalPages: data?.totalPages ?? 0,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePagedApiQuery;
|
export default usePagedApiQuery;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
@@ -7,42 +6,14 @@ import themes from 'Styles/Themes';
|
|||||||
function createThemeSelector() {
|
function createThemeSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
|
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
|
||||||
(theme) => theme
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTheme = () => {
|
const useTheme = () => {
|
||||||
const selectedTheme = useSelector(createThemeSelector());
|
return useSelector(createThemeSelector());
|
||||||
const [resolvedTheme, setResolvedTheme] = useState(selectedTheme);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedTheme !== 'auto') {
|
|
||||||
setResolvedTheme(selectedTheme);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const applySystemTheme = () => {
|
|
||||||
setResolvedTheme(
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
applySystemTheme();
|
|
||||||
|
|
||||||
window
|
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
.addEventListener('change', applySystemTheme);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window
|
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
.removeEventListener('change', applySystemTheme);
|
|
||||||
};
|
|
||||||
}, [selectedTheme]);
|
|
||||||
|
|
||||||
return resolvedTheme;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useTheme;
|
export default useTheme;
|
||||||
|
|||||||
@@ -57,12 +57,11 @@
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: calc(3 * 60px);
|
max-height: calc(3 * 50px);
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
line-height: 60px;
|
line-height: 50px;
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +82,6 @@
|
|||||||
|
|
||||||
.alternateTitlesIconContainer {
|
.alternateTitlesIconContainer {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,10 +144,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
|
|||||||
<div className={styles.poster}>
|
<div className={styles.poster}>
|
||||||
<div className={styles.posterContainer}>
|
<div className={styles.posterContainer}>
|
||||||
{isSelectMode ? (
|
{isSelectMode ? (
|
||||||
<SeriesIndexPosterSelect
|
<SeriesIndexPosterSelect seriesId={seriesId} />
|
||||||
seriesId={seriesId}
|
|
||||||
titleSlug={titleSlug}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{status === 'ended' ? (
|
{status === 'ended' ? (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
|
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
@@ -122,8 +123,31 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||||||
setIsDeleteSeriesModalOpen(false);
|
setIsDeleteSeriesModalOpen(false);
|
||||||
}, [setIsDeleteSeriesModalOpen]);
|
}, [setIsDeleteSeriesModalOpen]);
|
||||||
|
|
||||||
|
const [selectState, selectDispatch] = useSelect();
|
||||||
|
|
||||||
|
const onSelectPress = useCallback(
|
||||||
|
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
|
||||||
|
window.open(`/series/${titleSlug}`, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
|
selectDispatch({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
id: seriesId,
|
||||||
|
isSelected: !selectState.selectedState[seriesId],
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[seriesId, selectState.selectedState, selectDispatch, titleSlug]
|
||||||
|
);
|
||||||
|
|
||||||
const link = `/series/${titleSlug}`;
|
const link = `/series/${titleSlug}`;
|
||||||
|
|
||||||
|
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
|
||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`,
|
height: `${posterHeight}px`,
|
||||||
@@ -132,9 +156,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.posterContainer} title={title}>
|
<div className={styles.posterContainer} title={title}>
|
||||||
{isSelectMode ? (
|
{isSelectMode ? <SeriesIndexPosterSelect seriesId={seriesId} /> : null}
|
||||||
<SeriesIndexPosterSelect seriesId={seriesId} titleSlug={titleSlug} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Label className={styles.controls}>
|
<Label className={styles.controls}>
|
||||||
<SpinnerIconButton
|
<SpinnerIconButton
|
||||||
@@ -177,7 +199,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Link className={styles.link} style={elementStyle} to={link}>
|
<Link className={styles.link} style={elementStyle} {...linkProps}>
|
||||||
<SeriesPoster
|
<SeriesPoster
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
images={images}
|
images={images}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
width: 100%;
|
width: 36px;
|
||||||
height: 100%;
|
height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkContainer {
|
.checkContainer {
|
||||||
|
|||||||
@@ -7,23 +7,15 @@ import styles from './SeriesIndexPosterSelect.css';
|
|||||||
|
|
||||||
interface SeriesIndexPosterSelectProps {
|
interface SeriesIndexPosterSelectProps {
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
titleSlug: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SeriesIndexPosterSelect({
|
function SeriesIndexPosterSelect(props: SeriesIndexPosterSelectProps) {
|
||||||
seriesId,
|
const { seriesId } = props;
|
||||||
titleSlug,
|
|
||||||
}: SeriesIndexPosterSelectProps) {
|
|
||||||
const [selectState, selectDispatch] = useSelect();
|
const [selectState, selectDispatch] = useSelect();
|
||||||
const isSelected = selectState.selectedState[seriesId];
|
const isSelected = selectState.selectedState[seriesId];
|
||||||
|
|
||||||
const onSelectPress = useCallback(
|
const onSelectPress = useCallback(
|
||||||
(event: SyntheticEvent<HTMLElement, PointerEvent>) => {
|
(event: SyntheticEvent<HTMLElement, PointerEvent>) => {
|
||||||
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
|
|
||||||
window.open(`${window.Sonarr.urlBase}/series/${titleSlug}`, '_blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
selectDispatch({
|
selectDispatch({
|
||||||
@@ -33,7 +25,7 @@ function SeriesIndexPosterSelect({
|
|||||||
shiftKey,
|
shiftKey,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[seriesId, titleSlug, isSelected, selectDispatch]
|
[seriesId, isSelected, selectDispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ function GeneralSettings() {
|
|||||||
enableSsl={settings.enableSsl}
|
enableSsl={settings.enableSsl}
|
||||||
sslPort={settings.sslPort}
|
sslPort={settings.sslPort}
|
||||||
sslCertPath={settings.sslCertPath}
|
sslCertPath={settings.sslCertPath}
|
||||||
sslKeyPath={settings.sslKeyPath}
|
|
||||||
sslCertPassword={settings.sslCertPassword}
|
sslCertPassword={settings.sslCertPassword}
|
||||||
launchBrowser={settings.launchBrowser}
|
launchBrowser={settings.launchBrowser}
|
||||||
onInputChange={handleInputChange}
|
onInputChange={handleInputChange}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ interface HostSettingsProps {
|
|||||||
applicationUrl: PendingSection<General>['applicationUrl'];
|
applicationUrl: PendingSection<General>['applicationUrl'];
|
||||||
enableSsl: PendingSection<General>['enableSsl'];
|
enableSsl: PendingSection<General>['enableSsl'];
|
||||||
sslPort: PendingSection<General>['sslPort'];
|
sslPort: PendingSection<General>['sslPort'];
|
||||||
sslKeyPath: PendingSection<General>['sslKeyPath'];
|
|
||||||
sslCertPath: PendingSection<General>['sslCertPath'];
|
sslCertPath: PendingSection<General>['sslCertPath'];
|
||||||
sslCertPassword: PendingSection<General>['sslCertPassword'];
|
sslCertPassword: PendingSection<General>['sslCertPassword'];
|
||||||
launchBrowser: PendingSection<General>['launchBrowser'];
|
launchBrowser: PendingSection<General>['launchBrowser'];
|
||||||
@@ -35,7 +34,6 @@ function HostSettings({
|
|||||||
enableSsl,
|
enableSsl,
|
||||||
sslPort,
|
sslPort,
|
||||||
sslCertPath,
|
sslCertPath,
|
||||||
sslKeyPath,
|
|
||||||
sslCertPassword,
|
sslCertPassword,
|
||||||
launchBrowser,
|
launchBrowser,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
@@ -144,46 +142,33 @@ function HostSettings({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{enableSsl.value ? (
|
{enableSsl.value ? (
|
||||||
<>
|
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
<FormLabel>{translate('SslCertPath')}</FormLabel>
|
||||||
<FormLabel>{translate('SslCertPath')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="sslCertPath"
|
name="sslCertPath"
|
||||||
helpText={translate('SslCertPathHelpText')}
|
helpText={translate('SslCertPathHelpText')}
|
||||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...sslCertPath}
|
{...sslCertPath}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
{enableSsl.value ? (
|
||||||
<FormLabel>{translate('SslKeyPath')}</FormLabel>
|
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||||
|
<FormLabel>{translate('SslCertPassword')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TEXT}
|
type={inputTypes.PASSWORD}
|
||||||
name="sslKeyPath"
|
name="sslCertPassword"
|
||||||
helpText={translate('SslKeyPathHelpText')}
|
helpText={translate('SslCertPasswordHelpText')}
|
||||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...sslKeyPath}
|
{...sslCertPassword}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
|
||||||
<FormLabel>{translate('SslCertPassword')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.PASSWORD}
|
|
||||||
name="sslCertPassword"
|
|
||||||
helpText={translate('SslCertPasswordHelpText')}
|
|
||||||
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...sslCertPassword}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isWindowsService ? null : (
|
{isWindowsService ? null : (
|
||||||
|
|||||||
@@ -116,27 +116,6 @@ const fileDateOptions: EnhancedSelectInputValue<string>[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const seasonPackUpgradeOptions: EnhancedSelectInputValue<string>[] = [
|
|
||||||
{
|
|
||||||
key: 'all',
|
|
||||||
get value() {
|
|
||||||
return translate('All');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'threshold',
|
|
||||||
get value() {
|
|
||||||
return translate('Threshold');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'any',
|
|
||||||
get value() {
|
|
||||||
return translate('Any');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function MediaManagement() {
|
function MediaManagement() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const showAdvancedSettings = useShowAdvancedSettings();
|
const showAdvancedSettings = useShowAdvancedSettings();
|
||||||
@@ -400,82 +379,6 @@ function MediaManagement() {
|
|||||||
{...settings.userRejectedExtensions}
|
{...settings.userRejectedExtensions}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{showAdvancedSettings && (
|
|
||||||
<>
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={showAdvancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('SeasonPackUpgradeAllowLabel')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="seasonPackUpgrade"
|
|
||||||
helpText={translate('SeasonPackUpgradeAllowHelpText')}
|
|
||||||
helpTextWarning={
|
|
||||||
settings.seasonPackUpgrade.value === 'any'
|
|
||||||
? translate('SeasonPackUpgradeAllowAnyWarning')
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
values={seasonPackUpgradeOptions}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
{...settings.seasonPackUpgrade}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{settings.seasonPackUpgrade.value === 'threshold' && (
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={showAdvancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('SeasonPackUpgradeThresholdLabel')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.FLOAT}
|
|
||||||
name="seasonPackUpgradeThreshold"
|
|
||||||
unit="%"
|
|
||||||
step={0.01}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
helpTexts={[
|
|
||||||
translate('SeasonPackUpgradeThresholdHelpText'),
|
|
||||||
translate(
|
|
||||||
'SeasonPackUpgradeThresholdHelpTextExample',
|
|
||||||
{
|
|
||||||
numberEpisodes: 2,
|
|
||||||
totalEpisodes: 8,
|
|
||||||
count: Math.ceil((100 * 2) / 8),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
translate(
|
|
||||||
'SeasonPackUpgradeThresholdHelpTextExample',
|
|
||||||
{
|
|
||||||
numberEpisodes: 3,
|
|
||||||
totalEpisodes: 12,
|
|
||||||
count: Math.ceil((100 * 3) / 12),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
translate(
|
|
||||||
'SeasonPackUpgradeThresholdHelpTextExample',
|
|
||||||
{
|
|
||||||
numberEpisodes: 6,
|
|
||||||
totalEpisodes: 24,
|
|
||||||
count: Math.ceil((100 * 6) / 24),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
{...settings.seasonPackUpgradeThreshold}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const newReleaseProfile: ReleaseProfile = {
|
|||||||
required: [],
|
required: [],
|
||||||
ignored: [],
|
ignored: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
excludedTags: [],
|
|
||||||
indexerId: 0,
|
indexerId: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,8 +76,7 @@ function EditReleaseProfileModalContent({
|
|||||||
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
|
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
|
||||||
useSelector(createReleaseProfileSelector(id));
|
useSelector(createReleaseProfileSelector(id));
|
||||||
|
|
||||||
const { name, enabled, required, ignored, tags, excludedTags, indexerId } =
|
const { name, enabled, required, ignored, tags, indexerId } = item;
|
||||||
item;
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const previousIsSaving = usePrevious(isSaving);
|
const previousIsSaving = usePrevious(isSaving);
|
||||||
@@ -204,19 +202,6 @@ function EditReleaseProfileModalContent({
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ExcludedTags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="excludedTags"
|
|
||||||
helpText={translate('ReleaseProfileExcludedTagSeriesHelpText')}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
{...excludedTags}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
</Form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
|
|||||||
required = [],
|
required = [],
|
||||||
ignored = [],
|
ignored = [],
|
||||||
tags,
|
tags,
|
||||||
excludedTags,
|
|
||||||
indexerId = 0,
|
indexerId = 0,
|
||||||
tagList,
|
tagList,
|
||||||
indexerList,
|
indexerList,
|
||||||
@@ -93,8 +92,6 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
|
|||||||
|
|
||||||
<TagList tags={tags} tagList={tagList} />
|
<TagList tags={tags} tagList={tagList} />
|
||||||
|
|
||||||
<TagList tags={excludedTags} tagList={tagList} kind={kinds.DANGER} />
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{enabled ? null : (
|
{enabled ? null : (
|
||||||
<Label kind={kinds.DISABLED} outline={true}>
|
<Label kind={kinds.DISABLED} outline={true}>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export interface TagDetailsModalContentProps {
|
|||||||
delayProfileIds: number[];
|
delayProfileIds: number[];
|
||||||
importListIds: number[];
|
importListIds: number[];
|
||||||
notificationIds: number[];
|
notificationIds: number[];
|
||||||
releaseProfileIds: number[];
|
restrictionIds: number[];
|
||||||
indexerIds: number[];
|
indexerIds: number[];
|
||||||
downloadClientIds: number[];
|
downloadClientIds: number[];
|
||||||
autoTagIds: number[];
|
autoTagIds: number[];
|
||||||
@@ -76,7 +76,7 @@ function TagDetailsModalContent({
|
|||||||
delayProfileIds = [],
|
delayProfileIds = [],
|
||||||
importListIds = [],
|
importListIds = [],
|
||||||
notificationIds = [],
|
notificationIds = [],
|
||||||
releaseProfileIds = [],
|
restrictionIds = [],
|
||||||
indexerIds = [],
|
indexerIds = [],
|
||||||
downloadClientIds = [],
|
downloadClientIds = [],
|
||||||
autoTagIds = [],
|
autoTagIds = [],
|
||||||
@@ -109,7 +109,7 @@ function TagDetailsModalContent({
|
|||||||
|
|
||||||
const releaseProfiles = useSelector(
|
const releaseProfiles = useSelector(
|
||||||
createMatchingItemSelector(
|
createMatchingItemSelector(
|
||||||
releaseProfileIds,
|
restrictionIds,
|
||||||
(state: AppState) => state.settings.releaseProfiles.items
|
(state: AppState) => state.settings.releaseProfiles.items
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ function Tag({ id, label }: TagProps) {
|
|||||||
importListIds = [],
|
importListIds = [],
|
||||||
notificationIds = [],
|
notificationIds = [],
|
||||||
restrictionIds = [],
|
restrictionIds = [],
|
||||||
excludedReleaseProfileIds = [],
|
|
||||||
indexerIds = [],
|
indexerIds = [],
|
||||||
downloadClientIds = [],
|
downloadClientIds = [],
|
||||||
autoTagIds = [],
|
autoTagIds = [],
|
||||||
@@ -36,17 +35,12 @@ function Tag({ id, label }: TagProps) {
|
|||||||
importListIds.length ||
|
importListIds.length ||
|
||||||
notificationIds.length ||
|
notificationIds.length ||
|
||||||
restrictionIds.length ||
|
restrictionIds.length ||
|
||||||
excludedReleaseProfileIds.length ||
|
|
||||||
indexerIds.length ||
|
indexerIds.length ||
|
||||||
downloadClientIds.length ||
|
downloadClientIds.length ||
|
||||||
autoTagIds.length ||
|
autoTagIds.length ||
|
||||||
seriesIds.length
|
seriesIds.length
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedReleaseProfileIds = Array.from(
|
|
||||||
new Set([...restrictionIds, ...excludedReleaseProfileIds]).values()
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleShowDetailsPress = useCallback(() => {
|
const handleShowDetailsPress = useCallback(() => {
|
||||||
setIsDetailsModalOpen(true);
|
setIsDetailsModalOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -101,7 +95,7 @@ function Tag({ id, label }: TagProps) {
|
|||||||
<TagInUse
|
<TagInUse
|
||||||
label={translate('ReleaseProfile')}
|
label={translate('ReleaseProfile')}
|
||||||
labelPlural={translate('ReleaseProfiles')}
|
labelPlural={translate('ReleaseProfiles')}
|
||||||
count={mergedReleaseProfileIds.length}
|
count={restrictionIds.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TagInUse
|
<TagInUse
|
||||||
@@ -132,7 +126,7 @@ function Tag({ id, label }: TagProps) {
|
|||||||
delayProfileIds={delayProfileIds}
|
delayProfileIds={delayProfileIds}
|
||||||
importListIds={importListIds}
|
importListIds={importListIds}
|
||||||
notificationIds={notificationIds}
|
notificationIds={notificationIds}
|
||||||
releaseProfileIds={mergedReleaseProfileIds}
|
restrictionIds={restrictionIds}
|
||||||
indexerIds={indexerIds}
|
indexerIds={indexerIds}
|
||||||
downloadClientIds={downloadClientIds}
|
downloadClientIds={downloadClientIds}
|
||||||
autoTagIds={autoTagIds}
|
autoTagIds={autoTagIds}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import updateSectionState from 'Utilities/State/updateSectionState';
|
|||||||
function createSetSettingValueReducer(section) {
|
function createSetSettingValueReducer(section) {
|
||||||
return (state, { payload }) => {
|
return (state, { payload }) => {
|
||||||
if (section === payload.section) {
|
if (section === payload.section) {
|
||||||
const { name, value, isFloat } = payload;
|
const { name, value } = payload;
|
||||||
const newState = getSectionState(state, section);
|
const newState = getSectionState(state, section);
|
||||||
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
|
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
|
||||||
|
|
||||||
@@ -15,12 +15,7 @@ function createSetSettingValueReducer(section) {
|
|||||||
let parsedValue = null;
|
let parsedValue = null;
|
||||||
|
|
||||||
if (_.isNumber(currentValue) && value != null) {
|
if (_.isNumber(currentValue) && value != null) {
|
||||||
// Use isFloat property to determine parsing method
|
parsedValue = parseInt(value);
|
||||||
if (isFloat) {
|
|
||||||
parsedValue = parseFloat(value);
|
|
||||||
} else {
|
|
||||||
parsedValue = parseInt(value);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
parsedValue = value;
|
parsedValue = value;
|
||||||
}
|
}
|
||||||
|
|||||||
221
frontend/src/Store/Actions/blocklistActions.js
Normal file
221
frontend/src/Store/Actions/blocklistActions.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import { set, updateItem } from './baseActions';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||||
|
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||||
|
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||||
|
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'blocklist';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
pageSize: 20,
|
||||||
|
sortKey: 'date',
|
||||||
|
sortDirection: sortDirections.DESCENDING,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
isRemoving: false,
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'series.sortTitle',
|
||||||
|
label: () => translate('SeriesTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceTitle',
|
||||||
|
label: () => translate('SourceTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: () => translate('Quality'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormats',
|
||||||
|
label: () => translate('Formats'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: () => translate('Date'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'indexer',
|
||||||
|
label: () => translate('Indexer'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'all',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: () => translate('All'),
|
||||||
|
filters: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'seriesIds',
|
||||||
|
label: () => translate('Series'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.SERIES
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'protocols',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.PROTOCOL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'blocklist.pageSize',
|
||||||
|
'blocklist.sortKey',
|
||||||
|
'blocklist.sortDirection',
|
||||||
|
'blocklist.selectedFilterKey',
|
||||||
|
'blocklist.columns'
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Types
|
||||||
|
|
||||||
|
export const FETCH_BLOCKLIST = 'blocklist/fetchBlocklist';
|
||||||
|
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
|
||||||
|
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
|
||||||
|
export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter';
|
||||||
|
export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption';
|
||||||
|
export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem';
|
||||||
|
export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems';
|
||||||
|
export const CLEAR_BLOCKLIST = 'blocklist/clearBlocklist';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchBlocklist = createThunk(FETCH_BLOCKLIST);
|
||||||
|
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
|
||||||
|
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
|
||||||
|
export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER);
|
||||||
|
export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION);
|
||||||
|
export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM);
|
||||||
|
export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS);
|
||||||
|
export const clearBlocklist = createAction(CLEAR_BLOCKLIST);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
...createServerSideCollectionHandlers(
|
||||||
|
section,
|
||||||
|
'/blocklist',
|
||||||
|
fetchBlocklist,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_BLOCKLIST,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER
|
||||||
|
}),
|
||||||
|
|
||||||
|
[REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'),
|
||||||
|
|
||||||
|
[REMOVE_BLOCKLIST_ITEMS]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
ids
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
dispatch(batchActions([
|
||||||
|
...ids.map((id) => {
|
||||||
|
return updateItem({
|
||||||
|
section,
|
||||||
|
id,
|
||||||
|
isRemoving: true
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
set({ section, isRemoving: true })
|
||||||
|
]));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/blocklist/bulk',
|
||||||
|
method: 'DELETE',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ ids })
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
// Don't use batchActions with thunks
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
|
||||||
|
dispatch(set({ section, isRemoving: false }));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
...ids.map((id) => {
|
||||||
|
return updateItem({
|
||||||
|
section,
|
||||||
|
id,
|
||||||
|
isRemoving: false
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
set({ section, isRemoving: false })
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[SET_BLOCKLIST_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||||
|
|
||||||
|
[CLEAR_BLOCKLIST]: createClearReducer(section, {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
}, defaultState, section);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
|
import * as blocklist from './blocklistActions';
|
||||||
import * as calendar from './calendarActions';
|
import * as calendar from './calendarActions';
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
@@ -27,6 +28,7 @@ import * as wanted from './wantedActions';
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
app,
|
app,
|
||||||
|
blocklist,
|
||||||
calendar,
|
calendar,
|
||||||
captcha,
|
captcha,
|
||||||
commands,
|
commands,
|
||||||
|
|||||||
@@ -28,17 +28,10 @@ import useEvents, { useFilters } from './useEvents';
|
|||||||
|
|
||||||
function LogsTable() {
|
function LogsTable() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const { data, error, isFetching, isFetched, isLoading, page, goToPage } =
|
||||||
records,
|
useEvents();
|
||||||
totalPages,
|
|
||||||
totalRecords,
|
const { records = [], totalPages = 0, totalRecords } = data ?? {};
|
||||||
error,
|
|
||||||
isFetching,
|
|
||||||
isFetched,
|
|
||||||
isLoading,
|
|
||||||
page,
|
|
||||||
goToPage,
|
|
||||||
} = useEvents();
|
|
||||||
|
|
||||||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
||||||
useEventOptions();
|
useEventOptions();
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import {
|
import Column from 'Components/Table/Column';
|
||||||
createOptionsStore,
|
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||||
PageableOptions,
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
} from 'Helpers/Hooks/useOptionsStore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
export type EventOptions = PageableOptions;
|
export interface EventOptions {
|
||||||
|
pageSize: number;
|
||||||
|
selectedFilterKey: string | number;
|
||||||
|
sortKey: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
const { useOptions, setOptions, setOption } = createOptionsStore<EventOptions>(
|
const { useOptions, setOptions, setOption } = createOptionsStore<EventOptions>(
|
||||||
'event_options',
|
'event_options',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { keepPreviousData } from '@tanstack/react-query';
|
import { keepPreviousData } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Filter } from 'App/State/AppState';
|
import { Filter } from 'App/State/AppState';
|
||||||
import usePage from 'Helpers/Hooks/usePage';
|
import usePage from 'Helpers/Hooks/usePage';
|
||||||
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
|
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
|
||||||
@@ -69,9 +69,17 @@ const useEvents = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleGoToPage = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
goToPage(page);
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
[goToPage, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...query,
|
...query,
|
||||||
goToPage,
|
goToPage: handleGoToPage,
|
||||||
page,
|
page,
|
||||||
refetch,
|
refetch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ interface Blocklist extends ModelBase {
|
|||||||
seriesId?: number;
|
seriesId?: number;
|
||||||
indexer?: string;
|
indexer?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
source?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Blocklist;
|
export default Blocklist;
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export interface GrabbedHistoryData {
|
|||||||
export interface DownloadFailedHistory {
|
export interface DownloadFailedHistory {
|
||||||
message: string;
|
message: string;
|
||||||
indexer?: string;
|
indexer?: string;
|
||||||
source?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadFolderImportedHistory {
|
export interface DownloadFolderImportedHistory {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export default interface General {
|
|||||||
branch: string;
|
branch: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
sslCertPath: string;
|
sslCertPath: string;
|
||||||
sslKeyPath: string;
|
|
||||||
sslCertPassword: string;
|
sslCertPassword: string;
|
||||||
urlBase: string;
|
urlBase: string;
|
||||||
instanceName: string;
|
instanceName: string;
|
||||||
|
|||||||
@@ -20,6 +20,4 @@ export default interface MediaManagement {
|
|||||||
extraFileExtensions: string;
|
extraFileExtensions: string;
|
||||||
userRejectedExtensions: string;
|
userRejectedExtensions: string;
|
||||||
enableMediaInfo: boolean;
|
enableMediaInfo: boolean;
|
||||||
seasonPackUpgrade: string;
|
|
||||||
seasonPackUpgradeThreshold: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ interface ReleaseProfile extends ModelBase {
|
|||||||
ignored: string[];
|
ignored: string[];
|
||||||
indexerId: number;
|
indexerId: number;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
excludedTags: number[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReleaseProfile;
|
export default ReleaseProfile;
|
||||||
|
|||||||
@@ -84,9 +84,8 @@
|
|||||||
|
|
||||||
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
|
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
|
||||||
|
|
||||||
<PathMap>$(MSBuildThisFileDirectory)=./</PathMap>
|
<PathMap>$(MSBuildProjectDirectory)=./$(MSBuildProjectName)/</PathMap>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Set the AssemblyConfiguration attribute for projects -->
|
<!-- Set the AssemblyConfiguration attribute for projects -->
|
||||||
<ItemGroup Condition="'$(SonarrProject)'=='true'">
|
<ItemGroup Condition="'$(SonarrProject)'=='true'">
|
||||||
<AssemblyAttribute Include="System.Reflection.AssemblyConfigurationAttribute">
|
<AssemblyAttribute Include="System.Reflection.AssemblyConfigurationAttribute">
|
||||||
@@ -123,11 +122,14 @@
|
|||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.1.20" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net8.0'">
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(SonarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
<PropertyGroup Condition="'$(SonarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||||
|
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
|
||||||
|
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
|
||||||
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
|
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
|
||||||
|
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Common.Http;
|
|
||||||
using NzbDrone.Test.Common;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Test.Http;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class UserAgentParserFixture : TestBase
|
|
||||||
{
|
|
||||||
// Ref *Arr `_userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({osName} {osVersion})";`
|
|
||||||
// Ref Mylar `Mylar3/' +str(hash) +'(' +vers +') +http://www.github.com/mylar3/mylar3/`
|
|
||||||
[TestCase("Mylar3/ 3ee23rh23irqfq (13123123) http://www.github.com/mylar3/mylar3/", "Mylar3")]
|
|
||||||
[TestCase("Lidarr/1.0.0.2300 (ubuntu 20.04)", "Lidarr")]
|
|
||||||
[TestCase("Radarr/1.0.0.2300 (ubuntu 20.04)", "Radarr")]
|
|
||||||
[TestCase("Readarr/1.0.0.2300 (ubuntu 20.04)", "Readarr")]
|
|
||||||
[TestCase("Sonarr/3.0.6.9999 (ubuntu 20.04)", "Sonarr")]
|
|
||||||
[TestCase("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", "Other")]
|
|
||||||
public void should_parse_user_agent(string userAgent, string parsedAgent)
|
|
||||||
{
|
|
||||||
UserAgentParser.ParseSource(userAgent).Should().Be(parsedAgent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -222,7 +222,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
|
|
||||||
private void RemovePidFile()
|
private void RemovePidFile()
|
||||||
{
|
{
|
||||||
if (OsInfo.IsNotWindows && _diskProvider.FolderExists(_appFolderInfo.AppDataFolder))
|
if (OsInfo.IsNotWindows)
|
||||||
{
|
{
|
||||||
_diskProvider.DeleteFile(Path.Combine(_appFolderInfo.AppDataFolder, "sonarr.pid"));
|
_diskProvider.DeleteFile(Path.Combine(_appFolderInfo.AppDataFolder, "sonarr.pid"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,17 +27,15 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||||||
_dataSpecialFolder = Environment.SpecialFolder.ApplicationData;
|
_dataSpecialFolder = Environment.SpecialFolder.ApplicationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startupContext.Args.TryGetValue(StartupContext.APPDATA, out var argsAppDataFolder))
|
if (startupContext.Args.ContainsKey(StartupContext.APPDATA))
|
||||||
{
|
{
|
||||||
AppDataFolder = argsAppDataFolder;
|
AppDataFolder = startupContext.Args[StartupContext.APPDATA];
|
||||||
Logger.Info("Data directory is being overridden to [{0}]", AppDataFolder);
|
Logger.Info("Data directory is being overridden to [{0}]", AppDataFolder);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AppDataFolder = Path.Combine(Environment.GetFolderPath(_dataSpecialFolder, Environment.SpecialFolderOption.DoNotVerify), "Sonarr");
|
AppDataFolder = Path.Combine(Environment.GetFolderPath(_dataSpecialFolder, Environment.SpecialFolderOption.DoNotVerify), "Sonarr");
|
||||||
LegacyAppDataFolder = OsInfo.IsOsx
|
LegacyAppDataFolder = Path.Combine(Environment.GetFolderPath(_dataSpecialFolder, Environment.SpecialFolderOption.DoNotVerify), "NzbDrone");
|
||||||
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), ".config", "NzbDrone")
|
|
||||||
: Path.Combine(Environment.GetFolderPath(_dataSpecialFolder, Environment.SpecialFolderOption.DoNotVerify), "NzbDrone");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StartUpFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName;
|
StartUpFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName;
|
||||||
|
|||||||
@@ -390,12 +390,5 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual HttpRequestBuilder AllowRedirect(bool allowAutoRedirect = true)
|
|
||||||
{
|
|
||||||
AllowAutoRedirect = allowAutoRedirect;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
public static class UserAgentParser
|
public static class UserAgentParser
|
||||||
{
|
{
|
||||||
private static readonly Regex AppSourceRegex = new(@"(?<agent>[a-z0-9]*)\/.*(?:\(.*\))?",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
||||||
|
|
||||||
public static string SimplifyUserAgent(string userAgent)
|
public static string SimplifyUserAgent(string userAgent)
|
||||||
{
|
{
|
||||||
if (userAgent == null || userAgent.StartsWith("Mozilla/5.0"))
|
if (userAgent == null || userAgent.StartsWith("Mozilla/5.0"))
|
||||||
@@ -16,17 +11,5 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
return userAgent;
|
return userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ParseSource(string userAgent)
|
|
||||||
{
|
|
||||||
var match = AppSourceRegex.Match(SimplifyUserAgent(userAgent) ?? string.Empty);
|
|
||||||
|
|
||||||
if (match.Groups["agent"].Success)
|
|
||||||
{
|
|
||||||
return match.Groups["agent"].Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Other";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,5 @@ public class ServerOptions
|
|||||||
public bool? EnableSsl { get; set; }
|
public bool? EnableSsl { get; set; }
|
||||||
public int? SslPort { get; set; }
|
public int? SslPort { get; set; }
|
||||||
public string SslCertPath { get; set; }
|
public string SslCertPath { get; set; }
|
||||||
public string SslKeyPath { get; set; }
|
|
||||||
public string SslCertPassword { get; set; }
|
public string SslCertPassword { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,13 @@
|
|||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.15" />
|
||||||
<PackageReference Include="Sentry" Version="4.0.2" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.2" />
|
|
||||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
|
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
|
|
||||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" />
|
||||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
|
|||||||
@@ -85,10 +85,6 @@ namespace NzbDrone.Core.Test.Configuration
|
|||||||
{
|
{
|
||||||
value = DateTime.Now.Millisecond;
|
value = DateTime.Now.Millisecond;
|
||||||
}
|
}
|
||||||
else if (propertyInfo.PropertyType == typeof(double))
|
|
||||||
{
|
|
||||||
value = (double)DateTime.Now.Millisecond;
|
|
||||||
}
|
|
||||||
else if (propertyInfo.PropertyType == typeof(bool))
|
else if (propertyInfo.PropertyType == typeof(bool))
|
||||||
{
|
{
|
||||||
value = true;
|
value = true;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using NUnit.Framework;
|
|||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
@@ -438,102 +437,5 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, new()).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_parseResultSingle, new()).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_reject_season_pack_when_mode_is_all_and_not_all_are_upgradable()
|
|
||||||
{
|
|
||||||
GivenProfile(new QualityProfile
|
|
||||||
{
|
|
||||||
Cutoff = Quality.Bluray1080p.Id,
|
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
|
||||||
UpgradeAllowed = true
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>()
|
|
||||||
.SetupGet(s => s.SeasonPackUpgrade)
|
|
||||||
.Returns(SeasonPackUpgradeType.All);
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
|
|
||||||
_parseResultMulti.Episodes = new List<Episode>
|
|
||||||
{
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 2 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
|
|
||||||
|
|
||||||
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
|
|
||||||
|
|
||||||
result.Accepted.Should().BeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_reject_for_season_pack_not_meeting_threshold()
|
|
||||||
{
|
|
||||||
GivenProfile(new QualityProfile
|
|
||||||
{
|
|
||||||
Cutoff = Quality.Bluray1080p.Id,
|
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
|
||||||
UpgradeAllowed = true
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>()
|
|
||||||
.SetupGet(s => s.SeasonPackUpgrade)
|
|
||||||
.Returns(SeasonPackUpgradeType.Threshold);
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>()
|
|
||||||
.SetupGet(s => s.SeasonPackUpgradeThreshold)
|
|
||||||
.Returns(90);
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
|
|
||||||
_parseResultMulti.Episodes = new List<Episode>
|
|
||||||
{
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 2 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 3 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 4 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 5 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 6 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 7 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 8 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 9 },
|
|
||||||
new Episode { EpisodeFile = null, EpisodeFileId = 0 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
|
|
||||||
|
|
||||||
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
|
|
||||||
|
|
||||||
result.Accepted.Should().BeFalse();
|
|
||||||
result.Reason.Should().Be(DownloadRejectionReason.DiskNotUpgrade);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_accept_season_pack_when_mode_is_any_and_at_least_one_upgradable()
|
|
||||||
{
|
|
||||||
GivenProfile(new QualityProfile
|
|
||||||
{
|
|
||||||
Cutoff = Quality.Bluray1080p.Id,
|
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
|
||||||
UpgradeAllowed = true
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocker.GetMock<IConfigService>()
|
|
||||||
.SetupGet(s => s.SeasonPackUpgrade)
|
|
||||||
.Returns(SeasonPackUpgradeType.Any);
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.FullSeason = true;
|
|
||||||
_parseResultMulti.Episodes = new List<Episode>
|
|
||||||
{
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.SDTV) }, EpisodeFileId = 1 },
|
|
||||||
new Episode { EpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.Bluray1080p) }, EpisodeFileId = 2 }
|
|
||||||
};
|
|
||||||
|
|
||||||
_parseResultMulti.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray1080p);
|
|
||||||
|
|
||||||
var result = Subject.IsSatisfiedBy(_parseResultMulti, new());
|
|
||||||
|
|
||||||
result.Accepted.Should().BeTrue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Moq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Download;
|
|
||||||
using NzbDrone.Core.Download.Clients.RQBit;
|
|
||||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.RQBitTests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class RQBitFixture : DownloadClientFixtureBase<RQBit>
|
|
||||||
{
|
|
||||||
protected RQBitTorrent _queued;
|
|
||||||
protected RQBitTorrent _downloading;
|
|
||||||
protected RQBitTorrent _failed;
|
|
||||||
protected RQBitTorrent _completed;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
Subject.Definition = new DownloadClientDefinition();
|
|
||||||
Subject.Definition.Settings = new RQbitSettings
|
|
||||||
{
|
|
||||||
Host = "127.0.0.1",
|
|
||||||
Port = 3030,
|
|
||||||
UseSsl = false
|
|
||||||
};
|
|
||||||
|
|
||||||
_queued = new RQBitTorrent
|
|
||||||
{
|
|
||||||
Hash = "HASH",
|
|
||||||
IsFinished = false,
|
|
||||||
IsActive = false,
|
|
||||||
Name = _title,
|
|
||||||
TotalSize = 1000,
|
|
||||||
RemainingSize = 1000,
|
|
||||||
Path = "somepath"
|
|
||||||
};
|
|
||||||
|
|
||||||
_downloading = new RQBitTorrent
|
|
||||||
{
|
|
||||||
Hash = "HASH",
|
|
||||||
IsFinished = false,
|
|
||||||
IsActive = true,
|
|
||||||
Name = _title,
|
|
||||||
TotalSize = 1000,
|
|
||||||
RemainingSize = 100,
|
|
||||||
Path = "somepath"
|
|
||||||
};
|
|
||||||
|
|
||||||
_failed = new RQBitTorrent
|
|
||||||
{
|
|
||||||
Hash = "HASH",
|
|
||||||
IsFinished = false,
|
|
||||||
IsActive = false,
|
|
||||||
Name = _title,
|
|
||||||
TotalSize = 1000,
|
|
||||||
RemainingSize = 1000,
|
|
||||||
Path = "somepath"
|
|
||||||
};
|
|
||||||
|
|
||||||
_completed = new RQBitTorrent
|
|
||||||
{
|
|
||||||
Hash = "HASH",
|
|
||||||
IsFinished = true,
|
|
||||||
IsActive = false,
|
|
||||||
Name = _title,
|
|
||||||
TotalSize = 1000,
|
|
||||||
RemainingSize = 0,
|
|
||||||
Path = "somepath"
|
|
||||||
};
|
|
||||||
|
|
||||||
Mocker.GetMock<ITorrentFileInfoReader>()
|
|
||||||
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<byte[]>()))
|
|
||||||
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void GivenSuccessfulDownload()
|
|
||||||
{
|
|
||||||
Mocker.GetMock<IRQbitProxy>()
|
|
||||||
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<RQbitSettings>()))
|
|
||||||
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
|
||||||
Mocker.GetMock<IRQbitProxy>()
|
|
||||||
.Setup(s => s.AddTorrentFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<RQbitSettings>()))
|
|
||||||
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void GivenTorrents(List<RQBitTorrent> torrents)
|
|
||||||
{
|
|
||||||
if (torrents == null)
|
|
||||||
{
|
|
||||||
torrents = new List<RQBitTorrent>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Mocker.GetMock<IRQbitProxy>()
|
|
||||||
.Setup(s => s.GetTorrents(It.IsAny<RQbitSettings>()))
|
|
||||||
.Returns(torrents);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void PrepareClientToReturnQueuedItem()
|
|
||||||
{
|
|
||||||
GivenTorrents(new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
_queued
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void PrepareClientToReturnDownloadingItem()
|
|
||||||
{
|
|
||||||
GivenTorrents(new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
_downloading
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void PrepareClientToReturnFailedItem()
|
|
||||||
{
|
|
||||||
GivenTorrents(new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
_failed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void PrepareClientToReturnCompletedItem()
|
|
||||||
{
|
|
||||||
GivenTorrents(new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
_completed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void queued_item_should_have_required_properties()
|
|
||||||
{
|
|
||||||
PrepareClientToReturnQueuedItem();
|
|
||||||
var item = Subject.GetItems().Single();
|
|
||||||
VerifyPaused(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void downloading_item_should_have_required_properties()
|
|
||||||
{
|
|
||||||
PrepareClientToReturnDownloadingItem();
|
|
||||||
var item = Subject.GetItems().Single();
|
|
||||||
VerifyDownloading(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void failed_item_should_have_required_properties()
|
|
||||||
{
|
|
||||||
PrepareClientToReturnFailedItem();
|
|
||||||
var item = Subject.GetItems().Single();
|
|
||||||
VerifyPaused(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void completed_download_should_have_required_properties()
|
|
||||||
{
|
|
||||||
PrepareClientToReturnCompletedItem();
|
|
||||||
var item = Subject.GetItems().Single();
|
|
||||||
VerifyCompleted(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Download_should_return_unique_id()
|
|
||||||
{
|
|
||||||
GivenSuccessfulDownload();
|
|
||||||
|
|
||||||
var remoteEpisode = CreateRemoteEpisode();
|
|
||||||
|
|
||||||
var id = await Subject.Download(remoteEpisode, CreateIndexer());
|
|
||||||
|
|
||||||
id.Should().NotBeNullOrEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
|
|
||||||
public async Task Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
|
|
||||||
{
|
|
||||||
GivenSuccessfulDownload();
|
|
||||||
|
|
||||||
var remoteEpisode = CreateRemoteEpisode();
|
|
||||||
remoteEpisode.Release.DownloadUrl = magnetUrl;
|
|
||||||
|
|
||||||
var id = await Subject.Download(remoteEpisode, CreateIndexer());
|
|
||||||
|
|
||||||
id.Should().Be(expectedHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_return_status_with_outputdirs()
|
|
||||||
{
|
|
||||||
var result = Subject.GetStatus();
|
|
||||||
|
|
||||||
result.IsLocalhost.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void GetItems_should_ignore_torrents_with_empty_path()
|
|
||||||
{
|
|
||||||
var torrents = new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
new RQBitTorrent { Name = "Test1", Hash = "Hash1", Path = "" },
|
|
||||||
new RQBitTorrent { Name = "Test2", Hash = "Hash2", Path = "/valid/path" }
|
|
||||||
};
|
|
||||||
|
|
||||||
GivenTorrents(torrents);
|
|
||||||
|
|
||||||
var items = Subject.GetItems();
|
|
||||||
|
|
||||||
items.Should().HaveCount(1);
|
|
||||||
items.First().Title.Should().Be("Test2");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void GetItems_should_ignore_torrents_with_relative_path()
|
|
||||||
{
|
|
||||||
var torrents = new List<RQBitTorrent>
|
|
||||||
{
|
|
||||||
new RQBitTorrent { Name = "Test1", Hash = "Hash1", Path = "./relative/path" },
|
|
||||||
new RQBitTorrent { Name = "Test2", Hash = "Hash2", Path = "/absolute/path" }
|
|
||||||
};
|
|
||||||
|
|
||||||
GivenTorrents(torrents);
|
|
||||||
|
|
||||||
var items = Subject.GetItems();
|
|
||||||
|
|
||||||
items.Should().HaveCount(1);
|
|
||||||
items.First().Title.Should().Be("Test2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
"TvrageID":"4055",
|
"TvrageID":"4055",
|
||||||
"ImdbID":"0320037",
|
"ImdbID":"0320037",
|
||||||
"InfoHash":"123",
|
"InfoHash":"123",
|
||||||
"Tags": ["Subtitles"],
|
|
||||||
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
|
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
|
||||||
},
|
},
|
||||||
"1234":{
|
"1234":{
|
||||||
@@ -55,9 +54,8 @@
|
|||||||
"TvrageID":"38472",
|
"TvrageID":"38472",
|
||||||
"ImdbID":"2377081",
|
"ImdbID":"2377081",
|
||||||
"InfoHash":"1234",
|
"InfoHash":"1234",
|
||||||
"Tags": [],
|
|
||||||
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
|
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
|
||||||
}},
|
}},
|
||||||
"results":"117927"
|
"results":"117927"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,34 +124,5 @@
|
|||||||
<newznab:attr name="nuked" value="0"/>
|
<newznab:attr name="nuked" value="0"/>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item>
|
|
||||||
<title>title</title>
|
|
||||||
<guid isPermaLink="true">subs=eng</guid>
|
|
||||||
<link>link</link>
|
|
||||||
<comments>comments</comments>
|
|
||||||
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
|
||||||
<category>category</category>
|
|
||||||
<description>description</description>
|
|
||||||
<enclosure url="url" length="500" type="application/x-nzb"/>
|
|
||||||
|
|
||||||
<newznab:attr name="haspretime" value="0"/>
|
|
||||||
<newznab:attr name="nuked" value="0"/>
|
|
||||||
<newznab:attr name="subs" value="Eng"/>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item>
|
|
||||||
<title>title</title>
|
|
||||||
<guid isPermaLink="true">subs=''</guid>
|
|
||||||
<link>link</link>
|
|
||||||
<comments>comments</comments>
|
|
||||||
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
|
||||||
<category>category</category>
|
|
||||||
<description>description</description>
|
|
||||||
<enclosure url="url" length="500" type="application/x-nzb"/>
|
|
||||||
|
|
||||||
<newznab:attr name="haspretime" value="0"/>
|
|
||||||
<newznab:attr name="nuked" value="0"/>
|
|
||||||
<newznab:attr name="subs" value=""/>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -15,24 +15,24 @@ namespace NzbDrone.Core.Test.Http
|
|||||||
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("http://eu.httpbin.org/get")]
|
[Test]
|
||||||
[TestCase("http://google.com/get")]
|
public void should_bypass_proxy()
|
||||||
[TestCase("http://localhost:8654/get")]
|
|
||||||
[TestCase("http://172.21.0.1:8989/api/v3/indexer/schema")]
|
|
||||||
public void should_bypass_proxy(string url)
|
|
||||||
{
|
{
|
||||||
var settings = GetProxySettings();
|
var settings = GetProxySettings();
|
||||||
|
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeTrue();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
|
||||||
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
|
||||||
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
|
||||||
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("http://bing.com/get")]
|
[Test]
|
||||||
[TestCase("http://172.3.0.1:8989/api/v3/indexer/schema")]
|
public void should_not_bypass_proxy()
|
||||||
public void should_not_bypass_proxy(string url)
|
|
||||||
{
|
{
|
||||||
var settings = GetProxySettings();
|
var settings = GetProxySettings();
|
||||||
|
|
||||||
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeFalse();
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
|
||||||
|
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
|
|||||||
torrentInfo.Container.Should().Be("MP4");
|
torrentInfo.Container.Should().Be("MP4");
|
||||||
torrentInfo.Codec.Should().Be("x264");
|
torrentInfo.Codec.Should().Be("x264");
|
||||||
torrentInfo.Resolution.Should().Be("SD");
|
torrentInfo.Resolution.Should().Be("SD");
|
||||||
|
|
||||||
torrentInfo.IndexerFlags.Should().HaveFlag(IndexerFlags.Subtitles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyBackOff()
|
private void VerifyBackOff()
|
||||||
|
|||||||
@@ -165,8 +165,6 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
[TestCase("nuked=0 attribute")]
|
[TestCase("nuked=0 attribute")]
|
||||||
[TestCase("prematch=1 and nuked=1 attributes", IndexerFlags.Scene, IndexerFlags.Nuked)]
|
[TestCase("prematch=1 and nuked=1 attributes", IndexerFlags.Scene, IndexerFlags.Nuked)]
|
||||||
[TestCase("haspretime=0 and nuked=0 attributes")]
|
[TestCase("haspretime=0 and nuked=0 attributes")]
|
||||||
[TestCase("subs=eng", IndexerFlags.Subtitles)]
|
|
||||||
[TestCase("subs=''")]
|
|
||||||
public async Task should_parse_indexer_flags(string releaseGuid, params IndexerFlags[] indexerFlags)
|
public async Task should_parse_indexer_flags(string releaseGuid, params IndexerFlags[] indexerFlags)
|
||||||
{
|
{
|
||||||
var feed = ReadAllText(@"Files/Indexers/Newznab/newznab_indexerflags.xml");
|
var feed = ReadAllText(@"Files/Indexers/Newznab/newznab_indexerflags.xml");
|
||||||
|
|||||||
@@ -60,8 +60,7 @@ namespace NzbDrone.Core.Test.Languages
|
|||||||
new object[] { 48, Language.Uzbek },
|
new object[] { 48, Language.Uzbek },
|
||||||
new object[] { 49, Language.Malay },
|
new object[] { 49, Language.Malay },
|
||||||
new object[] { 50, Language.Urdu },
|
new object[] { 50, Language.Urdu },
|
||||||
new object[] { 51, Language.Romansh },
|
new object[] { 51, Language.Romansh }
|
||||||
new object[] { 52, Language.Georgian }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static object[] ToIntCases =
|
public static object[] ToIntCases =
|
||||||
@@ -116,8 +115,7 @@ namespace NzbDrone.Core.Test.Languages
|
|||||||
new object[] { Language.Uzbek, 48 },
|
new object[] { Language.Uzbek, 48 },
|
||||||
new object[] { Language.Malay, 49 },
|
new object[] { Language.Malay, 49 },
|
||||||
new object[] { Language.Urdu, 50 },
|
new object[] { Language.Urdu, 50 },
|
||||||
new object[] { Language.Romansh, 51 },
|
new object[] { Language.Romansh, 51 }
|
||||||
new object[] { Language.Georgian, 52 }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -580,8 +580,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
[TestCase("ice", "IS")]
|
[TestCase("ice", "IS")]
|
||||||
[TestCase("dut", "NL")]
|
[TestCase("dut", "NL")]
|
||||||
[TestCase("nor", "NO")]
|
[TestCase("nor", "NO")]
|
||||||
[TestCase("geo", "KA")]
|
|
||||||
[TestCase("kat", "KA")]
|
|
||||||
public void should_format_languagecodes_properly(string language, string code)
|
public void should_format_languagecodes_properly(string language, string code)
|
||||||
{
|
{
|
||||||
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}";
|
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}";
|
||||||
|
|||||||
@@ -167,19 +167,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
result.Special.Should().BeTrue();
|
result.Special.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("[Underwater] Another OVA - The Other -Karma- (BD 1080p) [3A561D0E].mkv", "Another", 0)]
|
|
||||||
public void should_parse_absolute_specials_without_absolute_number(string postTitle, string title, int absoluteEpisodeNumber)
|
|
||||||
{
|
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
|
||||||
result.SeasonNumber.Should().Be(0);
|
|
||||||
result.EpisodeNumbers.Should().BeEmpty();
|
|
||||||
result.SeriesTitle.Should().Be(title);
|
|
||||||
result.FullSeason.Should().BeFalse();
|
|
||||||
result.Special.Should().BeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("[ANBU-AonE]_SeriesTitle_26-27_[F224EF26].avi", "SeriesTitle", 26, 27)]
|
[TestCase("[ANBU-AonE]_SeriesTitle_26-27_[F224EF26].avi", "SeriesTitle", 26, 27)]
|
||||||
[TestCase("[Doutei] Some Good, Anime Show - 01-12 [BD][720p-AAC]", "Some Good, Anime Show", 1, 12)]
|
[TestCase("[Doutei] Some Good, Anime Show - 01-12 [BD][720p-AAC]", "Some Good, Anime Show", 1, 12)]
|
||||||
[TestCase("Series Title (2010) - 01-02-03 - Episode Title (1) HDTV-720p", "Series Title (2010)", 1, 3)]
|
[TestCase("Series Title (2010) - 01-02-03 - Episode Title (1) HDTV-720p", "Series Title (2010)", 1, 3)]
|
||||||
|
|||||||
@@ -69,15 +69,5 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
var result = IsoLanguages.Find(isoCode);
|
var result = IsoLanguages.Find(isoCode);
|
||||||
result.Language.Should().Be(Language.Romansh);
|
result.Language.Should().Be(Language.Romansh);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("ka")]
|
|
||||||
[TestCase("geo")]
|
|
||||||
[TestCase("kat")]
|
|
||||||
[TestCase("ka-GE")]
|
|
||||||
public void should_return_georgian(string isoCode)
|
|
||||||
{
|
|
||||||
var result = IsoLanguages.Find(isoCode);
|
|
||||||
result.Language.Should().Be(Language.Georgian);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Title.the.Italy.Series.S02E01.720p.HDTV.x264-TLA")]
|
[TestCase("Title.the.Italy.Series.S02E01.720p.HDTV.x264-TLA")]
|
||||||
[TestCase("Series Title - S01E01 - Pilot.en.sub")]
|
[TestCase("Series Title - S01E01 - Pilot.en.sub")]
|
||||||
[TestCase("Series.Title.S01E01.SUBFRENCH.1080p.WEB.x264-GROUP")]
|
[TestCase("Series.Title.S01E01.SUBFRENCH.1080p.WEB.x264-GROUP")]
|
||||||
[TestCase("[Judas] Series Japanese Name (Series English Name) - S02E10 [1080P][HEVC x256 10bit][Eng-Subs] (Weekly)")]
|
|
||||||
public void should_parse_language_unknown(string postTitle)
|
public void should_parse_language_unknown(string postTitle)
|
||||||
{
|
{
|
||||||
var result = LanguageParser.ParseLanguages(postTitle);
|
var result = LanguageParser.ParseLanguages(postTitle);
|
||||||
@@ -175,7 +175,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[abc] My Series - 01 [简繁内封字幕]")]
|
[TestCase("[abc] My Series - 01 [简繁内封字幕]")]
|
||||||
[TestCase("[ABC字幕组] My Series - 01 [HDTV]")]
|
[TestCase("[ABC字幕组] My Series - 01 [HDTV]")]
|
||||||
[TestCase("[喵萌奶茶屋&LoliHouse] 拳愿阿修罗 / Kengan Ashura - 17 [WebRip 1080p HEVC-10bit AAC][中日双语字幕]")]
|
[TestCase("[喵萌奶茶屋&LoliHouse] 拳愿阿修罗 / Kengan Ashura - 17 [WebRip 1080p HEVC-10bit AAC][中日双语字幕]")]
|
||||||
[TestCase("Series.Towards.You.S01.国语音轨.2023.1080p.NF.WEB-DL.H264.DDP2.0-SeeWEB")]
|
|
||||||
public void should_parse_language_chinese(string postTitle)
|
public void should_parse_language_chinese(string postTitle)
|
||||||
{
|
{
|
||||||
var result = LanguageParser.ParseLanguages(postTitle);
|
var result = LanguageParser.ParseLanguages(postTitle);
|
||||||
@@ -530,37 +529,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
result.Should().Contain(Language.Romansh);
|
result.Should().Contain(Language.Romansh);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2025.S01.Georgian.1080p.WEB-DL.h264-RlsGrp")]
|
|
||||||
[TestCase("Title.the.Series.2025.S01.Geo.1080p.WEB-DL.h264-RlsGrp")]
|
|
||||||
[TestCase("Title.the.Series.2025.S01.KA.1080p.WEB-DL.h264-RlsGrp")]
|
|
||||||
public void should_parse_language_georgian(string postTitle)
|
|
||||||
{
|
|
||||||
var result = LanguageParser.ParseLanguages(postTitle);
|
|
||||||
result.Should().Contain(Language.Georgian);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("Title.the.Series.2025.S01.RU-KA.1080p.WEB-DL.h264-RlsGrp")]
|
|
||||||
public void should_parse_language_russian_and_georgian(string postTitle)
|
|
||||||
{
|
|
||||||
var result = LanguageParser.ParseLanguages(postTitle);
|
|
||||||
result.Should().BeEquivalentTo(new[] { Language.Russian, Language.Georgian });
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("The Boys S02 Eng Fre Ger Ita Por Spa 2160p WEBMux HDR10Plus HDR HEVC DDP SGF")]
|
|
||||||
public void should_parse_language_english_french_german_italian_portuguese_spanish(string postTitle)
|
|
||||||
{
|
|
||||||
var result = LanguageParser.ParseLanguages(postTitle);
|
|
||||||
result.Should().BeEquivalentTo(new[]
|
|
||||||
{
|
|
||||||
Language.English,
|
|
||||||
Language.French,
|
|
||||||
Language.German,
|
|
||||||
Language.Italian,
|
|
||||||
Language.Portuguese,
|
|
||||||
Language.Spanish
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||||
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bits DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
|
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bits DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
|
||||||
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bit DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
|
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bit DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
|
||||||
[TestCase("Seriesless (2016/S01/WEB-DL/1080p/AC3 5.1/DUAL/SUB)", "Seriesless (2016)", 1)]
|
[TestCase("Seriesless (2016/S01/WEB-DL/1080p/AC3 5.1/DUAL/SUB)", "Seriesless (2016)", 1)]
|
||||||
[TestCase("Series (1994) - Temporada 10", "Series (1994)", 10)]
|
|
||||||
public void should_parse_full_season_release(string postTitle, string title, int season)
|
public void should_parse_full_season_release(string postTitle, string title, int season)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
|||||||
@@ -179,8 +179,6 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Mini Title (Miniserie) (2024/S01E07/DSNP/WEB-DL/1080p/ESP/EAC3 5.1/ING/EAC3 5.1 Atmos/SUBS) SPWEB", "Mini Title (2024)", 1, 7)]
|
[TestCase("Mini Title (Miniserie) (2024/S01E07/DSNP/WEB-DL/1080p/ESP/EAC3 5.1/ING/EAC3 5.1 Atmos/SUBS) SPWEB", "Mini Title (2024)", 1, 7)]
|
||||||
[TestCase("Series.S006E18.Some.Title.Name-Part.1.1080p.WEB-DL.AAC2.0.H.264-Release", "Series", 6, 18)]
|
[TestCase("Series.S006E18.Some.Title.Name-Part.1.1080p.WEB-DL.AAC2.0.H.264-Release", "Series", 6, 18)]
|
||||||
[TestCase("Series.2006.S006E18.Some.Title.Name-Part.1.1080p.WEB-DL.AAC2.0.H.264-Release", "Series 2006", 6, 18)]
|
[TestCase("Series.2006.S006E18.Some.Title.Name-Part.1.1080p.WEB-DL.AAC2.0.H.264-Release", "Series 2006", 6, 18)]
|
||||||
[TestCase("我的人间烟火.Fireworks.Series.S01E01.2023.V2.1080p.WEB-DL.H264.AAC-SeeWEB", "Fireworks Series", 1, 1)]
|
|
||||||
[TestCase("Fireworks.Series.S01E01.2023.V2.1080p.WEB-DL.H264.AAC-SeeWEB", "Fireworks Series", 1, 1)]
|
|
||||||
|
|
||||||
// [TestCase("", "", 0, 0)]
|
// [TestCase("", "", 0, 0)]
|
||||||
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
||||||
|
|||||||
@@ -147,33 +147,5 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
result.SeriesTitle.Should().Be(title);
|
result.SeriesTitle.Should().Be(title);
|
||||||
result.FullSeason.Should().BeFalse();
|
result.FullSeason.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("【高清剧集网发布 www.DDHDTV.com】当我飞奔向你[全24集][国语音轨+简繁英字幕].Series.Towards.You.S01.2023.1080p.NF.WEB-DL.H264.DDP2.0-SeeWEB", "Series Towards You", "SeeWEB", 1)]
|
|
||||||
public void should_parse_full_season_releases(string postTitle, string title, string releaseGroup, int season)
|
|
||||||
{
|
|
||||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
|
||||||
|
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
|
||||||
result.Should().NotBeNull();
|
|
||||||
result.ReleaseGroup.Should().Be(releaseGroup);
|
|
||||||
result.EpisodeNumbers.Should().BeEmpty();
|
|
||||||
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
|
||||||
result.SeriesTitle.Should().Be(title);
|
|
||||||
result.FullSeason.Should().BeTrue();
|
|
||||||
result.SeasonNumber.Should().Be(season);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("【高清剧集网发布 www.DDHDTV.com】当我飞奔向你[第01-15集][国语配音+中文字幕].Series.Towards.You.S01.2023.2160p.YK.WEB-DL.H265.AAC-BlackTV", "Series Towards You", "BlackTV", 1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 })]
|
|
||||||
public void should_parse_multi_episode_release(string postTitle, string title, string releaseGroup, int season, int[] episodes)
|
|
||||||
{
|
|
||||||
postTitle = XmlCleaner.ReplaceUnicode(postTitle);
|
|
||||||
|
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
|
||||||
result.SeasonNumber.Should().Be(season);
|
|
||||||
result.EpisodeNumbers.Should().BeEquivalentTo(episodes);
|
|
||||||
result.SeriesTitle.Should().Be(title);
|
|
||||||
result.AbsoluteEpisodeNumbers.Should().BeEmpty();
|
|
||||||
result.FullSeason.Should().BeFalse();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using FizzWare.NBuilder;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Profiles.Releases;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Profiles
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class ReleaseProfileServiceFixture : CoreTest<ReleaseProfileService>
|
|
||||||
{
|
|
||||||
private List<ReleaseProfile> _releaseProfiles;
|
|
||||||
private ReleaseProfile _defaultReleaseProfile;
|
|
||||||
private ReleaseProfile _includedReleaseProfile;
|
|
||||||
private ReleaseProfile _excludedReleaseProfile;
|
|
||||||
private ReleaseProfile _includedAndExcludedReleaseProfile;
|
|
||||||
private int _providedTag;
|
|
||||||
private int _providedTagToExclude;
|
|
||||||
private int _notUsedTag;
|
|
||||||
private List<ReleaseProfile> _releaseProfilesWithoutTags;
|
|
||||||
private List<ReleaseProfile> _releaseProfilesWithProvidedTag;
|
|
||||||
private List<ReleaseProfile> _releaseProfilesWithProvidedTagOrWithoutTags;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
_providedTag = 1;
|
|
||||||
_providedTagToExclude = 2;
|
|
||||||
_notUsedTag = 3;
|
|
||||||
|
|
||||||
_releaseProfiles = Builder<ReleaseProfile>.CreateListOfSize(5)
|
|
||||||
.TheFirst(1)
|
|
||||||
.With(r => r.Required = ["required_one"])
|
|
||||||
.TheNext(1)
|
|
||||||
.With(r => r.Required = ["required_two"])
|
|
||||||
.With(r => r.Tags = [_providedTag])
|
|
||||||
.TheNext(1)
|
|
||||||
.With(r => r.Required = ["required_three"])
|
|
||||||
.With(r => r.ExcludedTags = [_providedTagToExclude])
|
|
||||||
.TheNext(1)
|
|
||||||
.With(r => r.Required = ["required_four"])
|
|
||||||
.With(r => r.Tags = [_providedTag])
|
|
||||||
.With(r => r.ExcludedTags = [_providedTagToExclude])
|
|
||||||
.TheNext(1)
|
|
||||||
.With(r => r.Required = ["required_five"])
|
|
||||||
.With(r => r.Tags = [_notUsedTag])
|
|
||||||
.Build()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
_defaultReleaseProfile = _releaseProfiles[0];
|
|
||||||
_includedReleaseProfile = _releaseProfiles[1];
|
|
||||||
_excludedReleaseProfile = _releaseProfiles[2];
|
|
||||||
_includedAndExcludedReleaseProfile = _releaseProfiles[3];
|
|
||||||
|
|
||||||
_releaseProfilesWithoutTags = [_defaultReleaseProfile, _excludedReleaseProfile];
|
|
||||||
_releaseProfilesWithProvidedTag = [_includedReleaseProfile, _includedAndExcludedReleaseProfile];
|
|
||||||
_releaseProfilesWithProvidedTagOrWithoutTags = [_defaultReleaseProfile, _includedReleaseProfile, _excludedReleaseProfile, _includedAndExcludedReleaseProfile];
|
|
||||||
|
|
||||||
Mocker.GetMock<IRestrictionRepository>()
|
|
||||||
.Setup(s => s.All())
|
|
||||||
.Returns(_releaseProfiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tags_should_return_release_profiles_without_tags_by_default()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTags([]);
|
|
||||||
releaseProfiles.Should().Equal(_releaseProfilesWithoutTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tags_should_return_release_profiles_with_provided_tag_or_without_tags()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTags([_providedTag]);
|
|
||||||
releaseProfiles.Should().Equal(_releaseProfilesWithProvidedTagOrWithoutTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tags_should_not_return_release_profiles_with_provided_tag_excluded()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTags([_providedTagToExclude]);
|
|
||||||
releaseProfiles.Should().NotContain(_excludedReleaseProfile);
|
|
||||||
releaseProfiles.Should().NotContain(_includedAndExcludedReleaseProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tag_should_return_release_profiles_with_provided_tag()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTag(_providedTag);
|
|
||||||
releaseProfiles.Should().Equal(_releaseProfilesWithProvidedTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_should_return_all_release_profiles()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.All();
|
|
||||||
releaseProfiles.Should().Equal(_releaseProfiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tags_should_not_return_release_profiles_with_a_provided_tag_both_included_and_excluded()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTags([_providedTag, _providedTagToExclude]);
|
|
||||||
releaseProfiles.Should().Equal([_defaultReleaseProfile, _includedReleaseProfile]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void all_for_tags_should_return_matching_tags_that_are_not_excluded_tags()
|
|
||||||
{
|
|
||||||
var releaseProfiles = Subject.AllForTags([_providedTag]);
|
|
||||||
releaseProfiles.Should().Equal([_defaultReleaseProfile, _includedReleaseProfile, _excludedReleaseProfile, _includedAndExcludedReleaseProfile]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Remove="StyleCop.Analyzers" />
|
<PackageReference Remove="StyleCop.Analyzers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
public IndexerFlags IndexerFlags { get; set; }
|
public IndexerFlags IndexerFlags { get; set; }
|
||||||
public ReleaseType ReleaseType { get; set; }
|
public ReleaseType ReleaseType { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public string Source { get; set; }
|
|
||||||
public string TorrentInfoHash { get; set; }
|
public string TorrentInfoHash { get; set; }
|
||||||
public List<Language> Languages { get; set; }
|
public List<Language> Languages { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
bool Blocklisted(int seriesId, ReleaseInfo release);
|
bool Blocklisted(int seriesId, ReleaseInfo release);
|
||||||
bool BlocklistedTorrentHash(int seriesId, string hash);
|
bool BlocklistedTorrentHash(int seriesId, string hash);
|
||||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||||
void Block(RemoteEpisode remoteEpisode, string message, string source);
|
void Block(RemoteEpisode remoteEpisode, string message);
|
||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
void Delete(List<int> ids);
|
void Delete(List<int> ids);
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
return _blocklistRepository.GetPaged(pagingSpec);
|
return _blocklistRepository.GetPaged(pagingSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Block(RemoteEpisode remoteEpisode, string message, string source)
|
public void Block(RemoteEpisode remoteEpisode, string message)
|
||||||
{
|
{
|
||||||
var blocklist = new Blocklist
|
var blocklist = new Blocklist
|
||||||
{
|
{
|
||||||
@@ -85,7 +85,6 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
Indexer = remoteEpisode.Release.Indexer,
|
Indexer = remoteEpisode.Release.Indexer,
|
||||||
Protocol = remoteEpisode.Release.DownloadProtocol,
|
Protocol = remoteEpisode.Release.DownloadProtocol,
|
||||||
Message = message,
|
Message = message,
|
||||||
Source = source,
|
|
||||||
Languages = remoteEpisode.ParsedEpisodeInfo.Languages
|
Languages = remoteEpisode.ParsedEpisodeInfo.Languages
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,7 +185,6 @@ namespace NzbDrone.Core.Blocklisting
|
|||||||
Indexer = message.Data.GetValueOrDefault("indexer"),
|
Indexer = message.Data.GetValueOrDefault("indexer"),
|
||||||
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")),
|
||||||
Message = message.Message,
|
Message = message.Message,
|
||||||
Source = message.Source,
|
|
||||||
Languages = message.Languages,
|
Languages = message.Languages,
|
||||||
TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent
|
TorrentInfoHash = message.TrackedDownload?.Protocol == DownloadProtocol.Torrent
|
||||||
? message.TrackedDownload.DownloadItem.DownloadId
|
? message.TrackedDownload.DownloadItem.DownloadId
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string Branch { get; }
|
string Branch { get; }
|
||||||
string ApiKey { get; }
|
string ApiKey { get; }
|
||||||
string SslCertPath { get; }
|
string SslCertPath { get; }
|
||||||
string SslKeyPath { get; }
|
|
||||||
string SslCertPassword { get; }
|
string SslCertPassword { get; }
|
||||||
string UrlBase { get; }
|
string UrlBase { get; }
|
||||||
string UiFolder { get; }
|
string UiFolder { get; }
|
||||||
@@ -258,7 +257,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10);
|
public int LogSizeLimit => Math.Min(Math.Max(_logOptions.SizeLimit ?? GetValueInt("LogSizeLimit", 1, persist: false), 0), 10);
|
||||||
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||||
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
|
||||||
public string SslKeyPath => _serverOptions.SslKeyPath ?? GetValue("SslKeyPath", "");
|
|
||||||
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
|
||||||
|
|
||||||
public string UrlBase
|
public string UrlBase
|
||||||
|
|||||||
@@ -263,18 +263,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
set { SetValue("UserRejectedExtensions", value); }
|
set { SetValue("UserRejectedExtensions", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public SeasonPackUpgradeType SeasonPackUpgrade
|
|
||||||
{
|
|
||||||
get { return GetValueEnum("SeasonPackUpgrade", SeasonPackUpgradeType.All); }
|
|
||||||
set { SetValue("SeasonPackUpgrade", value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public double SeasonPackUpgradeThreshold
|
|
||||||
{
|
|
||||||
get { return GetValueDouble("SeasonPackUpgradeThreshold", 100.0); }
|
|
||||||
set { SetValue("SeasonPackUpgradeThreshold", value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SetPermissionsLinux
|
public bool SetPermissionsLinux
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("SetPermissionsLinux", false); }
|
get { return GetValueBoolean("SetPermissionsLinux", false); }
|
||||||
@@ -429,11 +417,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
private double GetValueDouble(string key, double defaultValue = 0)
|
|
||||||
{
|
|
||||||
return Convert.ToDouble(GetValue(key, defaultValue), CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T GetValueEnum<T>(string key, T defaultValue)
|
private T GetValueEnum<T>(string key, T defaultValue)
|
||||||
{
|
{
|
||||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
||||||
@@ -471,11 +454,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
SetValue(key, value.ToString());
|
SetValue(key, value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetValue(string key, double value)
|
|
||||||
{
|
|
||||||
SetValue(key, value.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetValue(string key, Enum value)
|
private void SetValue(string key, Enum value)
|
||||||
{
|
{
|
||||||
SetValue(key, value.ToString().ToLower());
|
SetValue(key, value.ToString().ToLower());
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
|
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
|
||||||
string UserRejectedExtensions { get; set; }
|
string UserRejectedExtensions { get; set; }
|
||||||
|
|
||||||
// Season Pack Upgrade (Media Management)
|
|
||||||
SeasonPackUpgradeType SeasonPackUpgrade { get; set; }
|
|
||||||
double SeasonPackUpgradeThreshold { get; set; }
|
|
||||||
|
|
||||||
// Permissions (Media Management)
|
// Permissions (Media Management)
|
||||||
bool SetPermissionsLinux { get; set; }
|
bool SetPermissionsLinux { get; set; }
|
||||||
string ChmodFolder { get; set; }
|
string ChmodFolder { get; set; }
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.SQLite;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common.Instrumentation;
|
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using Polly;
|
|
||||||
using Polly.Retry;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
@@ -45,31 +40,12 @@ namespace NzbDrone.Core.Datastore
|
|||||||
public class BasicRepository<TModel> : IBasicRepository<TModel>
|
public class BasicRepository<TModel> : IBasicRepository<TModel>
|
||||||
where TModel : ModelBase, new()
|
where TModel : ModelBase, new()
|
||||||
{
|
{
|
||||||
private static readonly ILogger Logger = NzbDroneLogger.GetLogger(typeof(BasicRepository<TModel>));
|
|
||||||
|
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly PropertyInfo _keyProperty;
|
private readonly PropertyInfo _keyProperty;
|
||||||
private readonly List<PropertyInfo> _properties;
|
private readonly List<PropertyInfo> _properties;
|
||||||
private readonly string _updateSql;
|
private readonly string _updateSql;
|
||||||
private readonly string _insertSql;
|
private readonly string _insertSql;
|
||||||
|
|
||||||
private static ResiliencePipeline RetryStrategy => new ResiliencePipelineBuilder()
|
|
||||||
.AddRetry(new RetryStrategyOptions
|
|
||||||
{
|
|
||||||
ShouldHandle = new PredicateBuilder().Handle<SQLiteException>(ex => ex.ResultCode == SQLiteErrorCode.Busy),
|
|
||||||
Delay = TimeSpan.FromMilliseconds(100),
|
|
||||||
MaxRetryAttempts = 3,
|
|
||||||
BackoffType = DelayBackoffType.Exponential,
|
|
||||||
UseJitter = true,
|
|
||||||
OnRetry = args =>
|
|
||||||
{
|
|
||||||
Logger.Warn(args.Outcome.Exception, "Failed writing to database. Retry #{0}", args.AttemptNumber);
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
protected readonly IDatabase _database;
|
protected readonly IDatabase _database;
|
||||||
protected readonly string _table;
|
protected readonly string _table;
|
||||||
|
|
||||||
@@ -210,9 +186,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
|
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
|
||||||
{
|
{
|
||||||
SqlBuilderExtensions.LogQuery(_insertSql, model);
|
SqlBuilderExtensions.LogQuery(_insertSql, model);
|
||||||
|
var multi = connection.QueryMultiple(_insertSql, model, transaction);
|
||||||
var multi = RetryStrategy.Execute(static (state, _) => state.connection.QueryMultiple(state._insertSql, state.model, state.transaction), (connection, _insertSql, model, transaction));
|
|
||||||
|
|
||||||
var multiRead = multi.Read();
|
var multiRead = multi.Read();
|
||||||
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||||
_keyProperty.SetValue(model, id);
|
_keyProperty.SetValue(model, id);
|
||||||
@@ -407,7 +381,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
|
|
||||||
SqlBuilderExtensions.LogQuery(sql, model);
|
SqlBuilderExtensions.LogQuery(sql, model);
|
||||||
|
|
||||||
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.model, transaction: state.transaction), (connection, sql, model, transaction));
|
connection.Execute(sql, model, transaction: transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
|
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
|
||||||
@@ -419,7 +393,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
SqlBuilderExtensions.LogQuery(sql, model);
|
SqlBuilderExtensions.LogQuery(sql, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.models, transaction: state.transaction), (connection, sql, models, transaction));
|
connection.Execute(sql, models, transaction: transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual SqlBuilder PagedBuilder() => Builder();
|
protected virtual SqlBuilder PagedBuilder() => Builder();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using NzbDrone.Common.Instrumentation;
|
|||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||||
public class DatabaseEngineVersionCheck : ForwardOnlyMigration
|
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
||||||
{
|
{
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
@@ -22,6 +22,11 @@ namespace NzbDrone.Core.Datastore.Migration
|
|||||||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Down()
|
||||||
|
{
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||||
{
|
{
|
||||||
using (var versionCmd = conn.CreateCommand())
|
using (var versionCmd = conn.CreateCommand())
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using NzbDrone.Core.Datastore.Migration.Framework;
|
|||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
[Migration(220)]
|
[Migration(229)]
|
||||||
public class enable_season_pack_seeding_goal : NzbDroneMigrationBase
|
public class enable_season_pack_seeding_goal : NzbDroneMigrationBase
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
@@ -21,45 +21,37 @@ namespace NzbDrone.Core.Datastore.Migration
|
|||||||
{
|
{
|
||||||
var updatedIndexers = new List<object>();
|
var updatedIndexers = new List<object>();
|
||||||
|
|
||||||
using (var selectCommand = conn.CreateCommand())
|
using var selectCommand = conn.CreateCommand();
|
||||||
|
|
||||||
|
selectCommand.Transaction = tran;
|
||||||
|
selectCommand.CommandText = "SELECT * FROM \"Indexers\"";
|
||||||
|
|
||||||
|
using var reader = selectCommand.ExecuteReader();
|
||||||
|
|
||||||
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
selectCommand.Transaction = tran;
|
var idIndex = reader.GetOrdinal("Id");
|
||||||
selectCommand.CommandText = "SELECT * FROM \"Indexers\"";
|
var settingsIndex = reader.GetOrdinal("Settings");
|
||||||
|
|
||||||
using (var reader = selectCommand.ExecuteReader())
|
var id = reader.GetInt32(idIndex);
|
||||||
|
var settings = Json.Deserialize<Dictionary<string, object>>(reader.GetString(settingsIndex));
|
||||||
|
|
||||||
|
if (settings.TryGetValue("seedCriteria", out var seedCriteriaToken) && seedCriteriaToken is JObject seedCriteria)
|
||||||
{
|
{
|
||||||
var indexerSettings = new List<(int Id, string Settings)>();
|
if (seedCriteria?["seasonPackSeedTime"] != null)
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
{
|
||||||
var idIndex = reader.GetOrdinal("Id");
|
seedCriteria["seasonPackSeedGoal"] = 1;
|
||||||
var settingsIndex = reader.GetOrdinal("Settings");
|
|
||||||
|
|
||||||
indexerSettings.Add((reader.GetInt32(idIndex), reader.GetString(settingsIndex)));
|
if (seedCriteria["seedRatio"] != null)
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var indexerSetting in indexerSettings)
|
|
||||||
{
|
|
||||||
var settings = Json.Deserialize<Dictionary<string, object>>(indexerSetting.Settings);
|
|
||||||
|
|
||||||
if (settings.TryGetValue("seedCriteria", out var seedCriteriaToken) && seedCriteriaToken is JObject seedCriteria)
|
|
||||||
{
|
{
|
||||||
if (seedCriteria?["seasonPackSeedTime"] != null)
|
seedCriteria["seasonPackSeedRatio"] = seedCriteria["seedRatio"];
|
||||||
{
|
|
||||||
seedCriteria["seasonPackSeedGoal"] = 1;
|
|
||||||
|
|
||||||
if (seedCriteria["seedRatio"] != null)
|
|
||||||
{
|
|
||||||
seedCriteria["seasonPackSeedRatio"] = seedCriteria["seedRatio"];
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedIndexers.Add(new
|
|
||||||
{
|
|
||||||
Settings = settings.ToJson(),
|
|
||||||
Id = indexerSetting.Id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatedIndexers.Add(new
|
||||||
|
{
|
||||||
|
Settings = settings.ToJson(),
|
||||||
|
Id = id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
using FluentMigrator;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
|
||||||
{
|
|
||||||
[Migration(221)]
|
|
||||||
public class add_exclusion_tags_to_release_profiles : NzbDroneMigrationBase
|
|
||||||
{
|
|
||||||
protected override void MainDbUpgrade()
|
|
||||||
{
|
|
||||||
Alter.Table("ReleaseProfiles").AddColumn("ExcludedTags").AsString().NotNullable().WithDefaultValue("[]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using FluentMigrator;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
|
||||||
{
|
|
||||||
[Migration(223)]
|
|
||||||
public class add_source_to_blocklist : NzbDroneMigrationBase
|
|
||||||
{
|
|
||||||
protected override void MainDbUpgrade()
|
|
||||||
{
|
|
||||||
Alter.Table("Blocklist").AddColumn("Source").AsString().Nullable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ using FluentMigrator.Runner.Generators;
|
|||||||
using FluentMigrator.Runner.Initialization;
|
using FluentMigrator.Runner.Initialization;
|
||||||
using FluentMigrator.Runner.Processors;
|
using FluentMigrator.Runner.Processors;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
|
|
||||||
@@ -19,10 +20,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
private readonly ILoggerProvider _migrationLoggerProvider;
|
||||||
|
|
||||||
public MigrationController(Logger logger)
|
public MigrationController(Logger logger,
|
||||||
|
ILoggerProvider migrationLoggerProvider)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_migrationLoggerProvider = migrationLoggerProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
@@ -31,13 +35,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||||
|
|
||||||
|
ServiceProvider serviceProvider;
|
||||||
|
|
||||||
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||||
|
|
||||||
var serviceProvider = new ServiceCollection()
|
serviceProvider = new ServiceCollection()
|
||||||
.AddLogging(b => b.AddNLog())
|
.AddLogging(b => b.AddNLog())
|
||||||
.AddFluentMigratorCore()
|
.AddFluentMigratorCore()
|
||||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||||
.ConfigureRunner(builder => builder
|
.ConfigureRunner(
|
||||||
|
builder => builder
|
||||||
.AddPostgres()
|
.AddPostgres()
|
||||||
.AddNzbDroneSQLite()
|
.AddNzbDroneSQLite()
|
||||||
.WithGlobalConnectionString(connectionString)
|
.WithGlobalConnectionString(connectionString)
|
||||||
|
|||||||
@@ -4,14 +4,9 @@ using FluentMigrator.Builders.Create;
|
|||||||
using FluentMigrator.Builders.Create.Table;
|
using FluentMigrator.Builders.Create.Table;
|
||||||
using FluentMigrator.Runner;
|
using FluentMigrator.Runner;
|
||||||
using FluentMigrator.Runner.BatchParser;
|
using FluentMigrator.Runner.BatchParser;
|
||||||
using FluentMigrator.Runner.Generators;
|
|
||||||
using FluentMigrator.Runner.Generators.SQLite;
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
using FluentMigrator.Runner.Initialization;
|
|
||||||
using FluentMigrator.Runner.Processors;
|
|
||||||
using FluentMigrator.Runner.Processors.SQLite;
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
{
|
{
|
||||||
@@ -31,40 +26,23 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddParameter(this IDbCommand command, object value)
|
public static void AddParameter(this System.Data.IDbCommand command, object value)
|
||||||
{
|
{
|
||||||
var parameter = command.CreateParameter();
|
var parameter = command.CreateParameter();
|
||||||
parameter.Value = value;
|
parameter.Value = value;
|
||||||
command.Parameters.Add(parameter);
|
command.Parameters.Add(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder, bool binaryGuid = false, bool useStrictTables = false)
|
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddTransient<SQLiteBatchParser>()
|
.AddTransient<SQLiteBatchParser>()
|
||||||
.AddScoped<SQLiteDbFactory>()
|
.AddScoped<SQLiteDbFactory>()
|
||||||
.AddScoped<NzbDroneSQLiteProcessor>(sp =>
|
.AddScoped<NzbDroneSQLiteProcessor>()
|
||||||
{
|
|
||||||
var factory = sp.GetService<SQLiteDbFactory>();
|
|
||||||
var logger = sp.GetService<ILogger<NzbDroneSQLiteProcessor>>();
|
|
||||||
var options = sp.GetService<IOptionsSnapshot<ProcessorOptions>>();
|
|
||||||
var connectionStringAccessor = sp.GetService<IConnectionStringAccessor>();
|
|
||||||
var sqliteQuoter = new SQLiteQuoter(false);
|
|
||||||
return new NzbDroneSQLiteProcessor(factory, sp.GetService<SQLiteGenerator>(), logger, options, connectionStringAccessor, sp, sqliteQuoter);
|
|
||||||
})
|
|
||||||
.AddScoped<ISQLiteTypeMap>(_ => new NzbDroneSQLiteTypeMap(useStrictTables))
|
|
||||||
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
||||||
.AddScoped(
|
.AddScoped<SQLiteQuoter>()
|
||||||
sp =>
|
.AddScoped<SQLiteGenerator>()
|
||||||
{
|
|
||||||
var typeMap = sp.GetRequiredService<ISQLiteTypeMap>();
|
|
||||||
return new SQLiteGenerator(
|
|
||||||
new SQLiteQuoter(binaryGuid),
|
|
||||||
typeMap,
|
|
||||||
new OptionsWrapper<GeneratorOptions>(new GeneratorOptions()));
|
|
||||||
})
|
|
||||||
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
using System.Data;
|
|
||||||
using FluentMigrator.Runner.Generators.Base;
|
|
||||||
using FluentMigrator.Runner.Generators.SQLite;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework;
|
|
||||||
|
|
||||||
// Based on https://github.com/fluentmigrator/fluentmigrator/blob/v6.2.0/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteTypeMap.cs
|
|
||||||
public sealed class NzbDroneSQLiteTypeMap : TypeMapBase, ISQLiteTypeMap
|
|
||||||
{
|
|
||||||
public bool UseStrictTables { get; }
|
|
||||||
|
|
||||||
public NzbDroneSQLiteTypeMap(bool useStrictTables = false)
|
|
||||||
{
|
|
||||||
UseStrictTables = useStrictTables;
|
|
||||||
|
|
||||||
SetupTypeMaps();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be kept in sync with upstream
|
|
||||||
protected override void SetupTypeMaps()
|
|
||||||
{
|
|
||||||
SetTypeMap(DbType.Binary, "BLOB");
|
|
||||||
SetTypeMap(DbType.Byte, "INTEGER");
|
|
||||||
SetTypeMap(DbType.Int16, "INTEGER");
|
|
||||||
SetTypeMap(DbType.Int32, "INTEGER");
|
|
||||||
SetTypeMap(DbType.Int64, "INTEGER");
|
|
||||||
SetTypeMap(DbType.SByte, "INTEGER");
|
|
||||||
SetTypeMap(DbType.UInt16, "INTEGER");
|
|
||||||
SetTypeMap(DbType.UInt32, "INTEGER");
|
|
||||||
SetTypeMap(DbType.UInt64, "INTEGER");
|
|
||||||
|
|
||||||
if (!UseStrictTables)
|
|
||||||
{
|
|
||||||
SetTypeMap(DbType.Currency, "NUMERIC");
|
|
||||||
SetTypeMap(DbType.Decimal, "NUMERIC");
|
|
||||||
SetTypeMap(DbType.Double, "NUMERIC");
|
|
||||||
SetTypeMap(DbType.Single, "NUMERIC");
|
|
||||||
SetTypeMap(DbType.VarNumeric, "NUMERIC");
|
|
||||||
SetTypeMap(DbType.Date, "DATETIME");
|
|
||||||
SetTypeMap(DbType.DateTime, "DATETIME");
|
|
||||||
SetTypeMap(DbType.DateTime2, "DATETIME");
|
|
||||||
SetTypeMap(DbType.Time, "DATETIME");
|
|
||||||
SetTypeMap(DbType.Guid, "UNIQUEIDENTIFIER");
|
|
||||||
|
|
||||||
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
|
||||||
SetTypeMap(DbType.DateTimeOffset, "DATETIME");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetTypeMap(DbType.Currency, "TEXT");
|
|
||||||
SetTypeMap(DbType.Decimal, "TEXT");
|
|
||||||
SetTypeMap(DbType.Double, "REAL");
|
|
||||||
SetTypeMap(DbType.Single, "REAL");
|
|
||||||
SetTypeMap(DbType.VarNumeric, "TEXT");
|
|
||||||
SetTypeMap(DbType.Date, "TEXT");
|
|
||||||
SetTypeMap(DbType.DateTime, "TEXT");
|
|
||||||
SetTypeMap(DbType.DateTime2, "TEXT");
|
|
||||||
SetTypeMap(DbType.Time, "TEXT");
|
|
||||||
SetTypeMap(DbType.Guid, "TEXT");
|
|
||||||
|
|
||||||
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
|
||||||
SetTypeMap(DbType.DateTimeOffset, "TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
SetTypeMap(DbType.AnsiString, "TEXT");
|
|
||||||
SetTypeMap(DbType.String, "TEXT");
|
|
||||||
SetTypeMap(DbType.AnsiStringFixedLength, "TEXT");
|
|
||||||
SetTypeMap(DbType.StringFixedLength, "TEXT");
|
|
||||||
SetTypeMap(DbType.Boolean, "INTEGER");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetTypeMap(DbType type, int? size, int? precision)
|
|
||||||
{
|
|
||||||
return base.GetTypeMap(type, size: null, precision: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
{
|
{
|
||||||
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
||||||
{
|
{
|
||||||
private readonly SQLiteQuoter _quoter;
|
|
||||||
|
|
||||||
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
||||||
SQLiteGenerator generator,
|
SQLiteGenerator generator,
|
||||||
ILogger<NzbDroneSQLiteProcessor> logger,
|
ILogger<NzbDroneSQLiteProcessor> logger,
|
||||||
@@ -26,7 +24,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
SQLiteQuoter quoter)
|
SQLiteQuoter quoter)
|
||||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
||||||
{
|
{
|
||||||
_quoter = quoter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Process(AlterColumnExpression expression)
|
public override void Process(AlterColumnExpression expression)
|
||||||
@@ -38,7 +35,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Column {expression.Column.Name} does not exist on table {expression.TableName}.");
|
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
columnDefinitions[columnIndex] = expression.Column;
|
columnDefinitions[columnIndex] = expression.Column;
|
||||||
@@ -48,28 +45,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Process(AlterDefaultConstraintExpression expression)
|
|
||||||
{
|
|
||||||
var tableDefinition = GetTableSchema(expression.TableName);
|
|
||||||
|
|
||||||
var columnDefinitions = tableDefinition.Columns.ToList();
|
|
||||||
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.ColumnName);
|
|
||||||
|
|
||||||
if (columnIndex == -1)
|
|
||||||
{
|
|
||||||
throw new ApplicationException($"Column {expression.ColumnName} does not exist on table {expression.TableName}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var changedColumn = columnDefinitions[columnIndex];
|
|
||||||
changedColumn.DefaultValue = expression.DefaultValue;
|
|
||||||
|
|
||||||
columnDefinitions[columnIndex] = changedColumn;
|
|
||||||
|
|
||||||
tableDefinition.Columns = columnDefinitions;
|
|
||||||
|
|
||||||
ProcessAlterTable(tableDefinition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Process(DeleteColumnExpression expression)
|
public override void Process(DeleteColumnExpression expression)
|
||||||
{
|
{
|
||||||
var tableDefinition = GetTableSchema(expression.TableName);
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
@@ -87,7 +62,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnsToRemove.Any())
|
if (columnsToRemove.Any())
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Column {columnsToRemove.First()} does not exist on table {expression.TableName}.");
|
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
@@ -103,12 +78,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Column {expression.OldName} does not exist on table {expression.TableName}.");
|
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
||||||
{
|
{
|
||||||
throw new ApplicationException($"Column {expression.NewName} already exists on table {expression.TableName}.");
|
throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
||||||
@@ -153,20 +128,21 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// What is the cleanest way to do this? Add function to Generator?
|
// What is the cleanest way to do this? Add function to Generator?
|
||||||
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => _quoter.QuoteColumnName(c.Name)));
|
var quoter = new SQLiteQuoter();
|
||||||
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => _quoter.QuoteColumnName(c.Name)));
|
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
|
||||||
|
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name)));
|
||||||
|
|
||||||
Process(new CreateTableExpression { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||||
|
|
||||||
Process($"INSERT INTO {_quoter.QuoteTableName(tempTableName)} ({columnsToInsert}) SELECT {columnsToFetch} FROM {_quoter.QuoteTableName(tableName)}");
|
Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName)));
|
||||||
|
|
||||||
Process(new DeleteTableExpression { TableName = tableName });
|
Process(new DeleteTableExpression() { TableName = tableName });
|
||||||
|
|
||||||
Process(new RenameTableExpression { OldName = tempTableName, NewName = tableName });
|
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
|
||||||
|
|
||||||
foreach (var index in tableDefinition.Indexes)
|
foreach (var index in tableDefinition.Indexes)
|
||||||
{
|
{
|
||||||
Process(new CreateIndexExpression { Index = index });
|
Process(new CreateIndexExpression() { Index = index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,5 @@ public enum DownloadRejectionReason
|
|||||||
DiskCustomFormatCutoffMet,
|
DiskCustomFormatCutoffMet,
|
||||||
DiskCustomFormatScore,
|
DiskCustomFormatScore,
|
||||||
DiskCustomFormatScoreIncrement,
|
DiskCustomFormatScoreIncrement,
|
||||||
DiskUpgradesNotAllowed,
|
DiskUpgradesNotAllowed
|
||||||
DiskNotUpgrade
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user