1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

..

54 Commits

Author SHA1 Message Date
Weblate
6c8758c27a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translation: Servarr/Sonarr
2024-03-01 17:32:55 -08:00
Mark McDowall
086d3b5afa Increase migration timeout to 5 minutes 2024-03-01 17:26:26 -08:00
Mark McDowall
f8a0751775 New: Release Type (Single/Multi episode and Season Pack) for Custom Formats
Closes #3562
2024-03-01 17:26:26 -08:00
Mark McDowall
c99d81e79b New: Bypass archived history for failed downloads in SABnzbd 2024-03-01 17:26:10 -08:00
Mark McDowall
9fd193d2a8 New: URL Base setting for Media Server connections
Closes #4416
2024-03-01 17:24:52 -08:00
Bogdan
64f4365fe9 Update caniuse-lite 2024-03-01 17:24:41 -08:00
Bogdan
2773f77e1c New: Options button for Missing/Cutoff Unmet 2024-03-01 17:24:41 -08:00
Weblate
0a84b4a8e9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-03-01 17:24:35 -08:00
Weblate
236d8e4c50 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-02-27 20:44:11 -08:00
Mark McDowall
16d3827dbd Fixed: Processing updated episodes in series after refresh
Closes #6560
2024-02-27 20:42:27 -08:00
Bogdan
fa600e62e0 Fixed: Error when download client information is unavailable for Manual Interaction Required event
Closes #6558
(cherry picked from commit 173b1d6a4c0f2125c4413c0c09b269d87a1f1ee8)

Co-authored-by: Qstick <qstick@gmail.com>
2024-02-27 23:42:08 -05:00
Mark McDowall
fb6fc568c5 Fixed: Don't store seasons from import list items in database
Closes #6555
2024-02-27 20:40:39 -08:00
Bogdan
1f97679868 Fixed: Selection of last added custom filter
Plus some translations and typos
2024-02-27 20:40:33 -08:00
Mark McDowall
b34e0f8259 Fixed: Ignore language in split episode title 2024-02-27 00:33:07 -05:00
Mark McDowall
4c170d0452 New: Update anime episodes by season/episode number instead of absolute episode number
Closes #6547
2024-02-26 21:32:46 -08:00
Mark McDowall
6dc0a88004 New: Search for recently aired anime episodes with added absolute episode number
Closes #2044
2024-02-26 21:32:46 -08:00
Mark McDowall
33b44a8a53 New: Option to sync season monitoring state when importing from another Sonarr instance
Closes #6542
2024-02-26 21:32:39 -08:00
Mark McDowall
cb72e752f9 Fixed: Parsing of subtitle languages separated by dash
Closes #6494
2024-02-27 00:32:31 -05:00
Weblate
a11ee7bc11 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: EDUYO <eduardoestabiel@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Sadi A. Nogueira <contato@sadi.eti.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xupix <colinaubert25@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-02-26 21:32:04 -08:00
Stevie Robinson
98d60e1a8e Replace URLs in translations with tokens 2024-02-27 00:30:58 -05:00
Bruno Garcia
6377c688fc Update Sentry SDK add features
Co-authored-by: Stefan Jandl <reg@bitfox.at>
2024-02-27 00:30:32 -05:00
Mark McDowall
7a37f130f9 Bump version to 4.0.2 2024-02-26 20:54:08 -08:00
Mark McDowall
724dd7e733 Clean branch name to remove slashes 2024-02-20 21:23:52 -08:00
Weblate
e1be3b20e9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 闫锦彪 <yanjinbiaohere@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-02-20 20:20:43 -08:00
Sonarr
2f041f9ec1 Automated API Docs update
ignore-downstream
2024-02-20 20:20:28 -08:00
Mark McDowall
f10ccf587d Don't fail fast for integration tests 2024-02-20 20:12:57 -08:00
Mark McDowall
0242b40eda Use GitHubActionsTestLogger for test reporting 2024-02-20 20:12:57 -08:00
Bogdan
7a768b5d0f New: Indexer flags
Closes #2782
2024-02-20 23:12:45 -05:00
Mark McDowall
a57254640f Upgrade actions/setup-dotnet to v4 2024-02-20 20:10:36 -08:00
Mark McDowall
1a6f45bafd Upgrade actions/checkout to v4 2024-02-20 20:10:36 -08:00
Mark McDowall
c6071f6d81 Upgrade node to 20.11.1 2024-02-20 20:10:36 -08:00
Bogdan
2a47a237d4 Fix typo in log message matching by TVRage ID 2024-02-20 20:10:30 -08:00
Mark McDowall
a7607ac7d6 Fixed: Only match via TV Rage ID if TheTVDB ID is not available
Closes #6517
2024-02-20 20:10:17 -08:00
Mark McDowall
43797b326d New: Parse releases with season and episode numbers separated by a period
Closes #6492
2024-02-20 20:10:09 -08:00
Mark McDowall
5c4f829993 Fixed: Multi-word genres in Auto Tags
Fixed #6488
2024-02-20 20:10:02 -08:00
Bogdan
8dd8c95f36 Fixed: Avoid upgrades for custom formats cut-off already met 2024-02-20 20:09:52 -08:00
Weblate
6f6036a199 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Chaoshuai Lü <lcs@meta.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: wgwqd <wgwqd@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-02-20 20:08:04 -08:00
Mark McDowall
625e500132 Use 'paths-ignore' instead of 'path' with only negative matches 2024-02-17 22:39:18 -08:00
Weblate
39575b1248 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: David13467 <davidnow00@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hicabi Erdem <bilgi@hicabierdem.com>
Co-authored-by: Lucas <sixagag973@fkcod.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Steve Hansen <steve@hansenconsultancy.be>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: bogdan-rgb <b.hmelniczky@yandex.ru>
Co-authored-by: savin-msk <ns@a77.io>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-02-13 16:56:10 -08:00
abcasada
f1d343218c Hints for week column and short dates in UI settings
(cherry picked from commit 4558f552820b52bb1f9cd97fdabe03654ce9924a)
2024-02-13 16:47:48 -08:00
Qstick
b0829d5537 Fixed: Correctly persist calendar custom filter selection
ignore-downstream
2024-02-13 16:47:41 -08:00
Bogdan
965e7c22d9 Fixed: Reprocessing multi-language file in Manage Episodes 2024-02-13 16:47:31 -08:00
Bogdan
75535e61d9 Fixed: Reprocessing custom formats for file in Manual Import 2024-02-13 16:47:31 -08:00
Bogdan
c0b17d9345 Show download client ID as hint in select options 2024-02-13 16:46:55 -08:00
Bogdan
84e657482d Improve messaging on indexer specified download client is not available 2024-02-13 16:46:55 -08:00
Bogdan
ed27bcf213 Fixed: Refresh tags state to clear removed tags by housekeeping
(cherry picked from commit 2510f44c25bee6fede27d9fa2b9614176d12cb55)
2024-02-13 16:46:46 -08:00
Bogdan
9ee2fe6f5c Fix typo 2024-02-13 16:46:38 -08:00
Bogdan
d5e19b8c3c Prevent useless builds 2024-02-13 16:46:38 -08:00
Sonarr
2957b40512 Automated API Docs update
ignore-downstream
2024-02-13 16:46:26 -08:00
Bogdan
9f46fc923d Fix typo for Downloaded Episodes Scan command name 2024-02-13 16:46:02 -08:00
Bogdan
7dc1e47504 Fix translation token for DL client directory help text 2024-02-13 16:46:02 -08:00
Bogdan
d15c116f13 Fix translation keys for indexer validation 2024-02-13 16:46:02 -08:00
Bogdan
dd704579df Improve add/loading error notices 2024-02-13 16:46:02 -08:00
Bogdan
bd9d4b484c Update Custom Format Deletion confirmation message
Consistency with the rest of the Delete*MessageText
2024-02-13 16:46:02 -08:00
195 changed files with 3064 additions and 797 deletions

View File

@@ -27,7 +27,7 @@ runs:
using: 'composite'
steps:
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
- name: Setup Postgres
if: ${{ inputs.use_postgres }}
@@ -77,7 +77,7 @@ runs:
- name: Run tests
shell: bash
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx"
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
- name: Upload Test Results
if: ${{ !cancelled() }}
@@ -85,12 +85,3 @@ runs:
with:
name: results-${{ env.RESULTS_NAME }}
path: TestResults/*.trx
- name: Publish Test Results
uses: phoenix-actions/test-reporting@v12
with:
name: Test Results
output-to: step-summary
path: '*.trx'
reporter: dotnet-trx
working-directory: TestResults

View File

@@ -26,10 +26,10 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
id: setup-dotnet
- name: Create openapi.json

View File

@@ -5,9 +5,14 @@ on:
branches:
- develop
- main
paths-ignore:
- 'src/Sonarr.Api.*/openapi.json'
pull_request:
branches:
- develop
paths-ignore:
- 'src/NzbDrone.Core/Localization/Core/**'
- 'src/Sonarr.Api.*/openapi.json'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -15,9 +20,9 @@ concurrency:
env:
FRAMEWORK: net6.0
BRANCH: ${{ github.head_ref || github.ref_name }}
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.1
VERSION: 4.0.2
jobs:
backend:
@@ -28,10 +33,10 @@ jobs:
version: ${{ steps.variables.outputs.version }}
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
- name: Setup Environment Variables
id: variables
@@ -43,6 +48,8 @@ jobs:
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV"
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
@@ -102,12 +109,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Volta
uses: volta-cli/action@v4
- name: Yarn Intsall
- name: Yarn Install
run: yarn install
- name: Lint
@@ -144,7 +151,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test
@@ -159,7 +166,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test
@@ -173,6 +180,7 @@ jobs:
integration_test:
needs: backend
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
@@ -194,7 +202,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Test
uses: ./.github/actions/test

View File

@@ -41,7 +41,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Package
uses: ./.github/actions/package
@@ -60,7 +60,7 @@ jobs:
contents: write
steps:
- name: Check out
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download release artifacts
uses: actions/download-artifact@v4

View File

@@ -1,41 +0,0 @@
name: Publish Test Results
on:
workflow_run:
workflows: ['Build']
types:
- completed
permissions:
contents: read
actions: read
checks: write
jobs:
report:
if: ${{ github.event.workflow_run.conclusion != 'skipped' && github.event.workflow_run.conclusion != 'cancelled' }}
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
- name: Download Test Reports
uses: actions/download-artifact@v4
with:
path: test-results
pattern: results-*
merge-multiple: true
repository: ${{ github.event.repository.owner.login }}/${{ github.event.repository.name }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Test Results
uses: phoenix-actions/test-reporting@v12
with:
list-suites: failed
list-tests: failed
name: Test Results
only-summary: true
path: '*.trx'
reporter: dotnet-trx
working-directory: test-results

View File

@@ -9,6 +9,7 @@ import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
@@ -40,19 +41,21 @@ export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
downloadClients: DownloadClientAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
ui: UiSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
}
export default SettingsAppState;

View File

@@ -6,7 +6,7 @@ export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan';
export const DOWNLOADED_EPISODES_SCAN = 'DownloadedEpisodesScan';
export const EPISODE_SEARCH = 'EpisodeSearch';
export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';

View File

@@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService')} />
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} />
</Alert>
}

View File

@@ -1,3 +1,4 @@
import { maxBy } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -8,6 +9,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import FilterBuilderRow from './FilterBuilderRow';
import styles from './FilterBuilderModalContent.css';
@@ -49,7 +51,7 @@ class FilterBuilderModalContent extends Component {
if (id) {
dispatchSetFilter({ selectedFilterKey: id });
} else {
const last = customFilters[customFilters.length -1];
const last = maxBy(customFilters, 'id');
dispatchSetFilter({ selectedFilterKey: last.id });
}
@@ -107,7 +109,7 @@ class FilterBuilderModalContent extends Component {
this.setState({
labelErrors: [
{
message: 'Label is required'
message: translate('LabelIsRequired')
}
]
});
@@ -145,13 +147,13 @@ class FilterBuilderModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Custom Filter
{translate('CustomFilter')}
</ModalHeader>
<ModalBody>
<div className={styles.labelContainer}>
<div className={styles.label}>
Label
{translate('Label')}
</div>
<div className={styles.labelInputContainer}>
@@ -165,7 +167,9 @@ class FilterBuilderModalContent extends Component {
</div>
</div>
<div className={styles.label}>Filters</div>
<div className={styles.label}>
{translate('Filters')}
</div>
<div className={styles.rows}>
{
@@ -192,7 +196,7 @@ class FilterBuilderModalContent extends Component {
<ModalFooter>
<Button onPress={onCancelPress}>
Cancel
{translate('Cancel')}
</Button>
<SpinnerErrorButton
@@ -200,7 +204,7 @@ class FilterBuilderModalContent extends Component {
error={saveError}
onPress={this.onSaveFilterPress}
>
Save
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>

View File

@@ -37,8 +37,8 @@ class CustomFilter extends Component {
dispatchSetFilter
} = this.props;
// Assume that delete and then unmounting means the delete was successful.
// Moving this check to a ancestor would be more accurate, but would have
// Assume that delete and then unmounting means the deletion was successful.
// Moving this check to an ancestor would be more accurate, but would have
// more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' });

View File

@@ -26,7 +26,8 @@ function createMapStateToProps() {
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
value: downloadClient.name,
hint: `(${downloadClient.id})`
};
});

View File

@@ -11,6 +11,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
@@ -71,6 +72,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
@@ -279,6 +283,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),

View File

@@ -0,0 +1,62 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;

View File

@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
render() {
return (
<TagInput
delimiters={['Tab', 'Enter', ',']}
tagList={[]}
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}

View File

@@ -6,7 +6,13 @@ import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchSeries } from 'Store/Actions/seriesActions';
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -51,6 +57,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
@@ -61,6 +68,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
languagesIsPopulated,
importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated,
translationsIsPopulated
) => {
@@ -72,6 +80,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
languagesIsPopulated &&
importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated &&
translationsIsPopulated
);
@@ -86,6 +95,7 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error,
(state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
@@ -96,6 +106,7 @@ const selectErrors = createSelector(
qualityProfilesError,
languagesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
) => {
@@ -107,6 +118,7 @@ const selectErrors = createSelector(
qualityProfilesError ||
languagesError ||
importListsError ||
indexerFlagsError ||
systemStatusError ||
translationsError
);
@@ -120,6 +132,7 @@ const selectErrors = createSelector(
qualityProfilesError,
languagesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
};
@@ -174,6 +187,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() {
dispatch(fetchUISettings());
},
@@ -213,6 +229,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
@@ -238,6 +255,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles,
dispatchFetchLanguages,
dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
@@ -278,6 +296,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;

View File

@@ -16,6 +16,7 @@ export interface EpisodeFile extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
indexerFlags: number;
mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean;
}

View File

@@ -62,6 +62,7 @@ import {
faFileExport as fasFileExport,
faFileInvoice as farFileInvoice,
faFilter as fasFilter,
faFlag as fasFlag,
faFolderOpen as fasFolderOpen,
faForward as fasForward,
faHeart as fasHeart,
@@ -154,6 +155,7 @@ export const FILE_MISSING = fasFileCircleQuestion;
export const FILTER = fasFilter;
export const FINALE_SEASON = fasCirclePause;
export const FINALE_SERIES = fasCircleStop;
export const FLAG = fasFlag;
export const FOOTNOTE = fasAsterisk;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;

View File

@@ -12,6 +12,7 @@ export const PASSWORD = 'password';
export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';

View File

@@ -77,7 +77,7 @@ function InteractiveImportSelectFolderModalContent(
dispatch(
executeCommand({
name: commandNames.DOWNLOADED_EPSIODES_SCAN,
name: commandNames.DOWNLOADED_EPISODES_SCAN,
path: folder,
})
);

View File

@@ -0,0 +1,34 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
interface SelectIndexerFlagsModalProps {
isOpen: boolean;
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) {
const {
isOpen,
indexerFlags,
modalTitle,
onIndexerFlagsSelect,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectIndexerFlagsModalContent
indexerFlags={indexerFlags}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectIndexerFlagsModal;

View File

@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalBody': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,75 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
interface SelectIndexerFlagsModalContentProps {
indexerFlags: number;
modalTitle: string;
onIndexerFlagsSelect(indexerFlags: number): void;
onModalClose(): void;
}
function SelectIndexerFlagsModalContent(
props: SelectIndexerFlagsModalContentProps
) {
const { modalTitle, onIndexerFlagsSelect, onModalClose } = props;
const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags);
const onIndexerFlagsChange = useCallback(
({ value }: { value: number }) => {
setIndexerFlags(value);
},
[setIndexerFlags]
);
const onIndexerFlagsSelectWrapper = useCallback(() => {
onIndexerFlagsSelect(indexerFlags);
}, [indexerFlags, onIndexerFlagsSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SetIndexerFlagsModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>{translate('IndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectIndexerFlagsModalContent;

View File

@@ -29,6 +29,7 @@ import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import ImportMode from 'InteractiveImport/ImportMode';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
@@ -71,7 +72,8 @@ type SelectType =
| 'episode'
| 'releaseGroup'
| 'quality'
| 'language';
| 'language'
| 'indexerFlags';
type FilterExistingFiles = 'all' | 'new';
@@ -135,11 +137,21 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER,
title: () => translate('Rejections'),
}),
isSortable: true,
isVisible: true,
@@ -284,8 +296,18 @@ function InteractiveImportModalContent(
}
}
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return result;
}, [showSeries]);
}, [showSeries, items]);
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
@@ -343,6 +365,10 @@ function InteractiveImportModalContent(
key: 'language',
value: translate('SelectLanguage'),
},
{
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
];
if (allowSeriesChange) {
@@ -483,6 +509,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
episodeFileId,
} = item;
@@ -532,6 +559,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
});
return;
@@ -546,6 +574,7 @@ function InteractiveImportModalContent(
releaseGroup,
quality,
languages,
indexerFlags,
downloadId,
episodeFileId,
});
@@ -742,6 +771,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const orderedSelectedIds = items.reduce((acc: number[], file) => {
if (selectedIds.includes(file.id)) {
acc.push(file.id);
@@ -947,6 +992,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}

View File

@@ -12,9 +12,11 @@ import Episode from 'Episode/Episode';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
@@ -41,7 +43,8 @@ type SelectType =
| 'episode'
| 'releaseGroup'
| 'quality'
| 'language';
| 'language'
| 'indexerFlags';
type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean;
@@ -60,6 +63,7 @@ interface InteractiveImportRowProps {
size: number;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];
columns: Column[];
episodeFileId?: number;
@@ -84,6 +88,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
size,
customFormats,
customFormatScore,
indexerFlags,
rejections,
isReprocessing,
isSelected,
@@ -100,6 +105,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
[columns]
);
const isIndexerFlagsColumnVisible = useMemo(
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
@@ -306,6 +315,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItem({
id,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const seriesTitle = series ? series.title : '';
const isAnime = series?.seriesType === 'anime';
@@ -332,6 +362,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
return (
<TableRow>
@@ -448,6 +479,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
) : null}
</TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell>
{rejections.length ? (
<Popover
@@ -518,6 +571,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
</TableRow>
);
}

View File

@@ -13,6 +13,7 @@ export interface InteractiveImportCommandOptions {
releaseGroup?: string;
quality: QualityModel;
languages: Language[];
indexerFlags: number;
downloadId?: string;
episodeFileId?: number;
}
@@ -31,6 +32,7 @@ interface InteractiveImport extends ModelBase {
episodes: Episode[];
qualityWeight: number;
customFormats: object[];
indexerFlags: number;
rejections: Rejection[];
episodeFileId?: number;
}

View File

@@ -72,6 +72,15 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {

View File

@@ -44,7 +44,8 @@
cursor: default;
}
.rejected {
.rejected,
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'download': string;
'downloadIcon': string;
'indexer': string;
'indexerFlags': string;
'interactiveIcon': string;
'languages': string;
'manualDownloadContent': string;

View File

@@ -12,6 +12,7 @@ import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
@@ -98,6 +99,7 @@ interface InteractiveSearchRowProps {
mappedEpisodeNumbers?: number[];
mappedAbsoluteEpisodeNumbers?: number[];
mappedEpisodeInfo: ReleaseEpisode[];
indexerFlags: number;
rejections: string[];
episodeRequested: boolean;
downloadAllowed: boolean;
@@ -139,6 +141,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
mappedEpisodeNumbers,
mappedAbsoluteEpisodeNumbers,
mappedEpisodeInfo,
indexerFlags = 0,
rejections = [],
episodeRequested,
downloadAllowed,
@@ -254,10 +257,21 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
customFormats.length
)}
tooltip={<EpisodeFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover

View File

@@ -109,7 +109,7 @@ class OrganizePreviewModalContent extends Component {
{
!isFetching && error &&
<div>{translate('OrganizeLoadError')}</div>
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
}
{

View File

@@ -62,3 +62,9 @@
width: 55px;
}
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}

View File

@@ -6,6 +6,7 @@ interface CssExports {
'customFormatScore': string;
'episodeNumber': string;
'episodeNumberAnime': string;
'indexerFlags': string;
'languages': string;
'monitored': string;
'releaseGroup': string;

View File

@@ -1,22 +1,26 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeNumber from 'Episode/EpisodeNumber';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import IndexerFlags from 'Episode/IndexerFlags';
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
import { tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import formatRuntime from 'Utilities/Number/formatRuntime';
import translate from 'Utilities/String/translate';
import styles from './EpisodeRow.css';
class EpisodeRow extends Component {
@@ -77,6 +81,7 @@ class EpisodeRow extends Component {
releaseGroup,
customFormats,
customFormatScore,
indexerFlags,
alternateTitles,
columns
} = this.props;
@@ -211,7 +216,7 @@ class EpisodeRow extends Component {
customFormats.length
)}
tooltip={<EpisodeFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
);
@@ -322,6 +327,24 @@ class EpisodeRow extends Component {
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
@@ -381,6 +404,7 @@ EpisodeRow.propTypes = {
releaseGroup: PropTypes.string,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
mediaInfo: PropTypes.object,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -389,7 +413,8 @@ EpisodeRow.propTypes = {
EpisodeRow.defaultProps = {
alternateTitles: [],
customFormats: []
customFormats: [],
indexerFlags: 0
};
export default EpisodeRow;

View File

@@ -1,4 +1,3 @@
/* eslint max-params: 0 */
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
@@ -20,6 +19,7 @@ function createMapStateToProps() {
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
customFormats: episodeFile ? episodeFile.customFormats : [],
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
indexerFlags: episodeFile ? episodeFile.indexerFlags : 0,
alternateTitles: series.alternateTitles
};
}

View File

@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}

View File

@@ -62,7 +62,7 @@ class CustomFormats extends Component {
<FieldSet legend={translate('CustomFormats')}>
<PageSectionContent
errorMessage={translate('CustomFormatsLoadError')}
{...otherProps}c={true}
{...otherProps}
>
<div className={styles.customFormats}>
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
@@ -112,9 +113,9 @@ class EditCustomFormatModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddCustomFormatError')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import ClipboardButton from 'Components/Link/ClipboardButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -41,9 +42,9 @@ class ExportCustomFormatModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('CustomFormatsLoadError')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -11,7 +12,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, sizes } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportCustomFormatModalContent.css';
@@ -95,9 +96,9 @@ class ImportCustomFormatModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('CustomFormatsLoadError')}
</div>
</Alert>
}
{

View File

@@ -42,9 +42,9 @@ class AddSpecificationModalContent extends Component {
{
!isSchemaFetching && !!schemaError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddConditionError')}
</div>
</Alert>
}
{

View File

@@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div>
<div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div>
<div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div>
</Alert>
}

View File

@@ -43,9 +43,9 @@ class AddDownloadClientModalContent extends Component {
{
!isSchemaFetching && !!schemaError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddDownloadClientError')}
</div>
</Alert>
}
{

View File

@@ -41,7 +41,7 @@ class DownloadClient extends Component {
});
};
onDeleteDownloadClientModalClose= () => {
onDeleteDownloadClientModalClose = () => {
this.setState({ isDeleteDownloadClientModalOpen: false });
};

View File

@@ -69,9 +69,9 @@ class EditDownloadClientModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddDownloadClientError')}
</div>
</Alert>
}
{

View File

@@ -277,7 +277,7 @@ function ManageDownloadClientsModalContent(
isDisabled={!anySelected}
onPress={onTagsPress}
>
Set Tags
{translate('SetTags')}
</SpinnerButton>
</div>

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -52,9 +53,9 @@ function EditRemotePathMappingModalContent(props) {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddRemotePathMappingError')}
</div>
</Alert>
}
{

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -55,7 +54,7 @@ function createRemotePathMappingSelector() {
items
} = remotePathMappings;
const mapping = id ? _.find(items, { id }) : newRemotePathMapping;
const mapping = id ? items.find((i) => i.id === id) : newRemotePathMapping;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {

View File

@@ -47,7 +47,7 @@ class RemotePathMappings extends Component {
} = this.props;
return (
<FieldSet legend={translate('RemotePathMappings')} >
<FieldSet legend={translate('RemotePathMappings')}>
<PageSectionContent
errorMessage={translate('RemotePathMappingsLoadError')}
{...otherProps}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -50,9 +51,9 @@ function EditImportListExclusionModalContent(props) {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddImportListExclusionError')}
</div>
</Alert>
}
{

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -26,7 +25,7 @@ function createImportListExclusionSelector() {
items
} = importListExclusions;
const mapping = id ? _.find(items, { id }) : newImportListExclusion;
const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {

View File

@@ -44,9 +44,9 @@ class AddImportListModalContent extends Component {
{
!isSchemaFetching && !!schemaError ?
<div>
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</div> :
</Alert> :
null
}

View File

@@ -74,9 +74,9 @@ function EditImportListModalContent(props) {
{
!isFetching && !!error ?
<div>
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</div> :
</Alert> :
null
}

View File

@@ -41,7 +41,7 @@ class ImportList extends Component {
});
};
onDeleteImportListModalClose= () => {
onDeleteImportListModalClose = () => {
this.setState({ isDeleteImportListModalOpen: false });
};

View File

@@ -5,7 +5,6 @@ import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import AddImportListModal from './AddImportListModal';
import EditImportListModalConnector from './EditImportListModalConnector';
@@ -67,7 +66,7 @@ class ImportLists extends Component {
>
<div className={styles.lists}>
{
items.sort(sortByName).map((item) => {
items.map((item) => {
return (
<ImportList
key={item.id}

View File

@@ -4,16 +4,14 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import ImportLists from './ImportLists';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importLists,
(importLists) => {
return {
...importLists
};
}
createSortedSectionSelector('settings.importLists', sortByName),
(importLists) => importLists
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import ManageImportListsModalContent from './ManageImportListsModalContent';
interface ManageImportListsModalProps {
@@ -11,7 +12,7 @@ function ManageImportListsModal(props: ManageImportListsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<Modal isOpen={isOpen} size={sizes.EXTRA_LARGE} onModalClose={onModalClose}>
<ManageImportListsModalContent onModalClose={onModalClose} />
</Modal>
);

View File

@@ -2,13 +2,14 @@ import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchImportListOptions,
@@ -110,7 +111,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<div>{translate('UnableToLoadListOptions')}</div>
<Alert kind={kinds.DANGER}>{translate('ListOptionsLoadError')}</Alert>
) : null}
{hasSettings && !isFetching && !error ? (

View File

@@ -43,9 +43,9 @@ class AddIndexerModalContent extends Component {
{
!isSchemaFetching && !!schemaError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</div>
</Alert>
}
{

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -67,9 +68,9 @@ function EditIndexerModalContent(props) {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</div>
</Alert>
}
{

View File

@@ -42,7 +42,7 @@ class Indexer extends Component {
});
};
onDeleteIndexerModalClose= () => {
onDeleteIndexerModalClose = () => {
this.setState({ isDeleteIndexerModalOpen: false });
};

View File

@@ -4,6 +4,7 @@ import translate from 'Utilities/String/translate';
import styles from './TheTvdb.css';
function TheTvdb(props) {
debugger;
return (
<div className={styles.container}>
<img
@@ -15,8 +16,7 @@ function TheTvdb(props) {
<div className={styles.title}>
{translate('TheTvdb')}
</div>
<InlineMarkdown data={translate('SeriesAndEpisodeInformationIsProvidedByTheTVDB')} />
<InlineMarkdown data={translate('SeriesAndEpisodeInformationIsProvidedByTheTVDB', { url: 'https://www.thetvdb.com/subscribe' })} />
</div>
</div>

View File

@@ -1,11 +1,13 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddNotificationItem from './AddNotificationItem';
import styles from './AddNotificationModalContent.css';
@@ -39,9 +41,9 @@ class AddNotificationModalContent extends Component {
{
!isSchemaFetching && !!schemaError &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddNotificationError')}
</div>
</Alert>
}
{

View File

@@ -59,9 +59,9 @@ function EditNotificationModalContent(props) {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddNotificationError')}
</div>
</Alert>
}
{

View File

@@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
{
!isFetching && !!error ?
<div>
<Alert kind={kinds.DANGER}>
{translate('AddDelayProfileError')}
</div> :
</Alert> :
null
}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -33,7 +32,7 @@ function createDelayProfileSelector() {
items
} = delayProfiles;
const profile = id ? _.find(items, { id }) : newDelayProfile;
const profile = id ? items.find((i) => i.id === id) : newDelayProfile;
const settings = selectSettings(profile, pendingChanges, saveError);
return {

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -152,9 +153,9 @@ class EditQualityProfileModalContent extends Component {
{
!isFetching && !!error &&
<div>
<Alert kind={kinds.DANGER}>
{translate('AddQualityProfileError')}
</div>
</Alert>
}
{

View File

@@ -55,7 +55,7 @@ class QualityProfiles extends Component {
<FieldSet legend={translate('QualityProfiles')}>
<PageSectionContent
errorMessage={translate('QualityProfilesLoadError')}
{...otherProps}c={true}
{...otherProps}
>
<div className={styles.qualityProfiles}>
{

View File

@@ -48,7 +48,7 @@ function EditReleaseProfileModalContent(props) {
<Form {...otherProps}>
<FormGroup>
<FormLabel>Name</FormLabel>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -11,7 +10,6 @@ const newReleaseProfile = {
enabled: true,
required: [],
ignored: [],
includePreferredWhenRenaming: false,
tags: [],
indexerId: 0
};
@@ -30,7 +28,7 @@ function createMapStateToProps() {
items
} = releaseProfiles;
const profile = id ? _.find(items, { id }) : newReleaseProfile;
const profile = id ? items.find((i) => i.id === id) : newReleaseProfile;
const settings = selectSettings(profile, pendingChanges, saveError);
return {

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MiddleTruncate from 'react-middle-truncate';
@@ -43,7 +42,7 @@ class ReleaseProfile extends Component {
});
};
onDeleteReleaseProfileModalClose= () => {
onDeleteReleaseProfileModalClose = () => {
this.setState({ isDeleteReleaseProfileModalOpen: false });
};
@@ -72,7 +71,7 @@ class ReleaseProfile extends Component {
isDeleteReleaseProfileModalOpen
} = this.state;
const indexer = indexerId !== 0 && _.find(indexerList, { id: indexerId });
const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
return (
<Card

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
@@ -122,9 +123,9 @@ export default function EditAutoTaggingModalContent(props) {
{
!isFetching && !!error ?
<div>
<Alert kind={kinds.DANGER}>
{translate('AddAutoTagError')}
</div> :
</Alert> :
null
}

View File

@@ -55,9 +55,9 @@ export default function AddSpecificationModalContent(props) {
{
!isSchemaFetching && !!schemaError ?
<div>
<Alert kind={kinds.DANGER}>
{translate('AddConditionError')}
</div> :
</Alert> :
null
}

View File

@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div>
<div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div>
<div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div>
</Alert>
}

View File

@@ -40,7 +40,7 @@ class Tag extends Component {
});
};
onDeleteTagModalClose= () => {
onDeleteTagModalClose = () => {
this.setState({ isDeleteTagModalOpen: false });
};

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import Tags from './Tags';
function createMapStateToProps() {
@@ -25,6 +25,7 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchImportLists: fetchImportLists,
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
componentDidMount() {
const {
dispatchFetchTags,
dispatchFetchTagDetails,
dispatchFetchDelayProfiles,
dispatchFetchImportLists,
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
dispatchFetchDownloadClients
} = this.props;
dispatchFetchTags();
dispatchFetchTagDetails();
dispatchFetchDelayProfiles();
dispatchFetchImportLists();
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
}
MetadatasConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,

View File

@@ -31,19 +31,19 @@ export const firstDayOfWeekOptions = [
];
export const weekColumnOptions = [
{ key: 'ddd M/D', value: 'Tue 3/25' },
{ key: 'ddd MM/DD', value: 'Tue 03/25' },
{ key: 'ddd D/M', value: 'Tue 25/3' },
{ key: 'ddd DD/MM', value: 'Tue 25/03' }
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
];
const shortDateFormatOptions = [
{ key: 'MMM D YYYY', value: 'Mar 25 2014' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014' },
{ key: 'MM/D/YYYY', value: '03/25/2014' },
{ key: 'MM/DD/YYYY', value: '03/25/2014' },
{ key: 'DD/MM/YYYY', value: '25/03/2014' },
{ key: 'YYYY-MM-DD', value: '2014-03-25' }
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
{ key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
{ key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
];
const longDateFormatOptions = [

View File

@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};

View File

@@ -97,7 +97,7 @@ export const persistState = [
'calendar.view',
'calendar.selectedFilterKey',
'calendar.options',
'seriesIndex.customFilters'
'calendar.customFilters'
];
//

View File

@@ -129,6 +129,15 @@ export const defaultState = {
isVisible: false,
isSortable: true
},
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{
name: 'status',
label: () => translate('Status'),

View File

@@ -161,9 +161,12 @@ export const actionHandlers = handleThunks({
const episodeFile = data.find((f) => f.id === id);
props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet;
props.customFormats = episodeFile.customFormats;
props.customFormatScore = episodeFile.customFormatScore;
props.languages = file.languages;
props.quality = file.quality;
props.releaseGroup = file.releaseGroup;
props.indexerFlags = file.indexerFlags;
return updateItem({
section,

View File

@@ -162,6 +162,7 @@ export const actionHandlers = handleThunks({
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId
};
});

View File

@@ -1,4 +1,5 @@
import { createAction } from 'redux-actions';
import indexerFlags from 'Store/Actions/Settings/indexerFlags';
import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
import autoTaggings from './Settings/autoTaggings';
@@ -37,6 +38,7 @@ export * from './Settings/general';
export * from './Settings/importListOptions';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/indexers';
export * from './Settings/languages';
@@ -72,6 +74,7 @@ export const defaultState = {
importLists: importLists.defaultState,
importListExclusions: importListExclusions.defaultState,
importListOptions: importListOptions.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState,
languages: languages.defaultState,
@@ -116,6 +119,7 @@ export const actionHandlers = handleThunks({
...importLists.actionHandlers,
...importListExclusions.actionHandlers,
...importListOptions.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...indexers.actionHandlers,
...languages.actionHandlers,
@@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...importLists.reducers,
...importListExclusions.reducers,
...importListOptions.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers,
...indexers.reducers,
...languages.reducers,

View File

@@ -0,0 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;

View File

@@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getFilterValue from 'Utilities/Filter/getFilterValue';
@@ -180,6 +181,16 @@ class CutoffUnmet extends Component {
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}

View File

@@ -12,6 +12,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@@ -193,6 +194,16 @@ class Missing extends Component {
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}

View File

@@ -0,0 +1,6 @@
interface IndexerFlag {
id: number;
name: string;
}
export default IndexerFlag;

View File

@@ -28,8 +28,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@sentry/browser": "7.100.0",
"@types/node": "18.16.8",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
@@ -146,7 +145,7 @@
"worker-loader": "3.0.8"
},
"volta": {
"node": "16.17.0",
"node": "20.11.1",
"yarn": "1.22.19"
}
}

View File

@@ -98,6 +98,35 @@
<RootNamespace Condition="'$(SonarrProject)'=='true'">$(MSBuildProjectName.Replace('Sonarr','NzbDrone'))</RootNamespace>
</PropertyGroup>
<ItemGroup Condition="'$(TestProject)'!='true'">
<!-- Annotates .NET assemblies with repository information including SHA -->
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Sentry specific configuration: Only in Release mode -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
<!-- OrgSlug, ProjectSlug and AuthToken are required.
They can be set below, via argument to 'msbuild -p:' or environment variable -->
<SentryOrg></SentryOrg>
<SentryProject></SentryProject>
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
without the need to deploy the application with PDBs -->
<SentryUploadSymbols>true</SentryUploadSymbols>
<!-- Source Link settings -->
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
<EmbedAllSources>true</EmbedAllSources>
</PropertyGroup>
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />

View File

@@ -3,6 +3,7 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
<PackageReference Include="NBuilder" Version="6.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,6 +3,7 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="111.0.5563.6400" />
</ItemGroup>

View File

@@ -4,6 +4,7 @@ using System.Linq;
using FluentAssertions;
using NLog;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Test.Common;
@@ -26,7 +27,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[SetUp]
public void Setup()
{
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
}
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)

View File

@@ -2,6 +2,9 @@
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" />
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Common.Instrumentation
RegisterDebugger();
}
RegisterSentry(updateApp);
RegisterSentry(updateApp, appFolderInfo);
if (updateApp)
{
@@ -60,7 +60,7 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers();
}
private static void RegisterSentry(bool updateClient)
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
{
string dsn;
@@ -80,7 +80,7 @@ namespace NzbDrone.Common.Instrumentation
Target target;
try
{
target = new SentryTarget(dsn)
target = new SentryTarget(dsn, appFolderInfo)
{
Name = "sentryTarget",
Layout = "${message}"

View File

@@ -9,6 +9,7 @@ using NLog;
using NLog.Common;
using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry;
namespace NzbDrone.Common.Instrumentation.Sentry
@@ -96,7 +97,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
public bool FilterEvents { get; set; }
public bool SentryEnabled { get; set; }
public SentryTarget(string dsn)
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
{
_sdk = SentrySdk.Init(o =>
{
@@ -104,9 +105,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200;
o.Release = BuildInfo.Release;
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
o.Environment = BuildInfo.Branch;
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
o.AutoSessionTracking = true;
// Caches files in the event device is offline
// Sentry creates a 'sentry' sub directory, no need to concat here
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
// default environment is production
if (!RuntimeInfo.IsProduction)
{
if (RuntimeInfo.IsDevelopment)
{
o.Environment = "development";
}
else if (RuntimeInfo.IsTesting)
{
o.Environment = "testing";
}
else
{
o.Environment = "other";
}
}
});
InitializeScope();
@@ -125,7 +150,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new User
scope.User = new SentryUser
{
Id = HashUtil.AnonymousToken()
};
@@ -285,13 +310,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
}
var level = LoggingLevelMap[logEvent.Level];
var sentryEvent = new SentryEvent(logEvent.Exception)
{
Level = LoggingLevelMap[logEvent.Level],
Level = level,
Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage
};
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
{
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
// the 'unhandled' exception flag
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
}
sentryEvent.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint);

View File

@@ -11,7 +11,7 @@
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="Sentry" Version="4.0.2" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Text.Json" Version="6.0.8" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class release_typeFixture : MigrationTest<release_type>
{
[Test]
public void should_convert_single_episode_without_folder()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr.mkv",
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode);
}
[Test]
public void should_convert_single_episode_with_folder()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr/S01E05.mkv",
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode);
}
[Test]
public void should_convert_multi_episode_without_folder()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr.mkv",
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode);
}
[Test]
public void should_convert_multi_episode_with_folder()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr/S01E05E06.mkv",
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode);
}
[Test]
public void should_convert_season_pack_with_folder()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
OriginalFilePath = "Series.Title.S01.720p.HDTV.x265-Sonarr/S01E05.mkv",
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.SeasonPack);
}
[Test]
public void should_not_convert_episode_without_original_file_path()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("EpisodeFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
RelativePath = "Season 01/S01E05.mkv",
Size = 125.Megabytes(),
DateAdded = DateTime.UtcNow.AddDays(-5),
ReleaseGroup = "Sonarr",
Quality = new QualityModel(Quality.HDTV720p).ToJson(),
Languages = "[1]"
});
});
var items = db.Query<EpisodeFile203>("SELECT * FROM \"EpisodeFiles\"");
items.Should().HaveCount(1);
items.First().ReleaseType.Should().Be((int)ReleaseType.Unknown);
}
public class EpisodeFile203
{
public int Id { get; set; }
public int SeriesId { get; set; }
public int SeasonNumber { get; set; }
public string RelativePath { get; set; }
public long Size { get; set; }
public DateTime DateAdded { get; set; }
public string OriginalFilePath { get; set; }
public string SceneName { get; set; }
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public long IndexerFlags { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public List<int> Languages { get; set; }
public long ReleaseType { get; set; }
}
}
}

View File

@@ -8,6 +8,7 @@ using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.MediaFiles.Events;
@@ -48,6 +49,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!")));
_rejectedDecisions.ForEach(r => r.LocalEpisode.FileEpisodeInfo = new ParsedEpisodeInfo());
foreach (var episode in episodes)
{
@@ -58,7 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Episodes = new List<Episode> { episode },
Path = Path.Combine(series.Path, "30 Rock - S01E01 - Pilot.avi"),
Quality = new QualityModel(Quality.Bluray720p),
ReleaseGroup = "DRONE"
ReleaseGroup = "DRONE",
FileEpisodeInfo = new ParsedEpisodeInfo()
}));
}
@@ -66,6 +69,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Returns(new EpisodeFileMoveResult());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EpisodeHistory>());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
.With(d => d.OutputPath = new OsPath(outputPath))
.Build();

View File

@@ -441,6 +441,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")]
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);

Some files were not shown because too many files have changed in this diff Show More