From ad2721dc55f3233e4c299babe5744418bc530418 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 14 Jul 2023 16:55:25 -0700 Subject: [PATCH 01/18] Fixed: Ensure translations are fetched before loading app --- frontend/build/webpack.config.js | 4 ++++ frontend/src/App/App.js | 7 ++++--- frontend/src/Components/Page/ErrorPage.js | 4 ++++ frontend/src/Components/Page/PageConnector.js | 5 ++++- frontend/src/Utilities/String/translate.js | 8 +++++--- frontend/src/index.js | 10 ++++++++++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 82601a415..9201a5e76 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -84,6 +84,10 @@ module.exports = (env) => { hints: false }, + experiments: { + topLevelAwait: true + }, + plugins: [ new webpack.DefinePlugin({ __DEV__: !isProduction, diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js index 781b2ca10..ea29231c2 100644 --- a/frontend/src/App/App.js +++ b/frontend/src/App/App.js @@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector'; import ApplyTheme from './ApplyTheme'; import AppRoutes from './AppRoutes'; -function App({ store, history }) { +function App({ store, history, hasTranslationsError }) { return ( - + @@ -25,7 +25,8 @@ function App({ store, history }) { App.propTypes = { store: PropTypes.object.isRequired, - history: PropTypes.object.isRequired + history: PropTypes.object.isRequired, + hasTranslationsError: PropTypes.bool.isRequired }; export default App; diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js index fbbc1af25..e7b436d61 100644 --- a/frontend/src/Components/Page/ErrorPage.js +++ b/frontend/src/Components/Page/ErrorPage.js @@ -7,6 +7,7 @@ function ErrorPage(props) { const { version, isLocalStorageSupported, + hasTranslationsError, seriesError, customFiltersError, tagsError, @@ -19,6 +20,8 @@ function ErrorPage(props) { if (!isLocalStorageSupported) { errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; + } else if (hasTranslationsError) { + errorMessage = 'Failed to load translations from API'; } else if (seriesError) { errorMessage = getErrorMessage(seriesError, 'Failed to load series from API'); } else if (customFiltersError) { @@ -49,6 +52,7 @@ function ErrorPage(props) { ErrorPage.propTypes = { version: PropTypes.string.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired, + hasTranslationsError: PropTypes.bool.isRequired, seriesError: PropTypes.object, customFiltersError: PropTypes.object, tagsError: PropTypes.object, diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index a3127eddf..37c7bf8d0 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -220,6 +220,7 @@ class PageConnector extends Component { render() { const { + hasTranslationsError, isPopulated, hasError, dispatchFetchSeries, @@ -232,11 +233,12 @@ class PageConnector extends Component { ...otherProps } = this.props; - if (hasError || !this.state.isLocalStorageSupported) { + if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) { return ( ); } @@ -257,6 +259,7 @@ class PageConnector extends Component { } PageConnector.propTypes = { + hasTranslationsError: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired, diff --git a/frontend/src/Utilities/String/translate.js b/frontend/src/Utilities/String/translate.js index cef8ce047..802824524 100644 --- a/frontend/src/Utilities/String/translate.js +++ b/frontend/src/Utilities/String/translate.js @@ -10,9 +10,11 @@ function getTranslations() { let translations = {}; -getTranslations().then((data) => { - translations = data.strings; -}); +export function fetchTranslations() { + return getTranslations().then((data) => { + translations = data.strings; + }); +} export default function translate(key, tokens) { const translation = translations[key] || key; diff --git a/frontend/src/index.js b/frontend/src/index.js index 8c0c0752f..e9284a7de 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -2,6 +2,7 @@ import { createBrowserHistory } from 'history'; import React from 'react'; import { render } from 'react-dom'; import createAppStore from 'Store/createAppStore'; +import { fetchTranslations } from 'Utilities/String/translate'; import App from './App/App'; import './preload'; @@ -12,11 +13,20 @@ import './index.css'; const history = createBrowserHistory(); const store = createAppStore(history); +let hasTranslationsError = false; + +try { + await fetchTranslations(); + +} catch (error) { + hasTranslationsError = true; +} render( , document.getElementById('root') ); From dce6923b00146938703ac1d18724e9b537b317d9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 14 Jul 2023 17:01:01 -0700 Subject: [PATCH 02/18] Fixed: Parsing of anime title that contains multiple 3-digit numbers Closes #5801 --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 4d5107c92..ee96178f2 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -120,6 +120,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[HatSubs] Anime Title 1004 [E63F2984].mkv", "Anime Title", 1004, 0, 0)] [TestCase("Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)] [TestCase("[SubsPlease] Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)] + [TestCase("[CameEsp] Another Anime 100 - Another 100 Anime - 01 [720p][ESP-ENG][mkv]", "Another Anime 100 - Another 100 Anime", 1, 0, 0)] + [TestCase("[SubsPlease] Another Anime 100 - Another 100 Anime - 01 (1080p) [4E6B4518].mkv", "Another Anime 100 - Another 100 Anime", 1, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index dd9b96195..c9a6f282a 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -106,6 +106,11 @@ namespace NzbDrone.Core.Parser new Regex(@"^\[(?.+?)\][-_. ]?(?[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // TODO: WIP + // Anime - [SubGroup] Title with trailing 3-digit number and sub title - Absolute Episode Number + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Anime - [SubGroup] Title with trailing number Absolute Episode Number new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+)[_ ]+)(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), From c1f8c7b17ba5775a0f6f76cebc3173e03124d000 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Jul 2023 09:16:19 -0700 Subject: [PATCH 03/18] Use named keys for apply tags help text --- .../DownloadClients/Manage/Tags/TagsModalContent.tsx | 8 ++++---- .../ImportLists/Manage/Tags/TagsModalContent.tsx | 8 ++++---- .../Indexers/Indexers/Manage/Tags/TagsModalContent.tsx | 8 ++++---- src/NzbDrone.Core/Localization/Core/en.json | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx index 23b52d50f..65827ec15 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.tsx @@ -103,10 +103,10 @@ function TagsModalContent(props: TagsModalContentProps) { value={applyTags} values={applyTagsOptions} helpTexts={[ - translate('ApplyTagsHelpTexts1'), - translate('ApplyTagsHelpTexts2'), - translate('ApplyTagsHelpTexts3'), - translate('ApplyTagsHelpTexts4'), + translate('ApplyTagsHelpTextHowToApply'), + translate('ApplyTagsHelpTextAdd'), + translate('ApplyTagsHelpTextRemove'), + translate('ApplyTagsHelpTextReplace'), ]} onChange={onApplyTagsChange} /> diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx index a39d0ca86..185be46ca 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Tags/TagsModalContent.tsx @@ -101,10 +101,10 @@ function TagsModalContent(props: TagsModalContentProps) { value={applyTags} values={applyTagsOptions} helpTexts={[ - translate('ApplyTagsHelpTexts1'), - translate('ApplyTagsHelpTexts2'), - translate('ApplyTagsHelpTexts3'), - translate('ApplyTagsHelpTexts4'), + translate('ApplyTagsHelpTextHowToApply'), + translate('ApplyTagsHelpTextAdd'), + translate('ApplyTagsHelpTextRemove'), + translate('ApplyTagsHelpTextReplace'), ]} onChange={onApplyTagsChange} /> diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx index 3205c071a..c634684be 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx @@ -101,10 +101,10 @@ function TagsModalContent(props: TagsModalContentProps) { value={applyTags} values={applyTagsOptions} helpTexts={[ - translate('ApplyTagsHelpTexts1'), - translate('ApplyTagsHelpTexts2'), - translate('ApplyTagsHelpTexts3'), - translate('ApplyTagsHelpTexts4'), + translate('ApplyTagsHelpTextHowToApply'), + translate('ApplyTagsHelpTextAdd'), + translate('ApplyTagsHelpTextRemove'), + translate('ApplyTagsHelpTextReplace'), ]} onChange={onApplyTagsChange} /> diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index b5d6cc834..ebd7fcf13 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -7,10 +7,10 @@ "Apply": "Apply", "ApplyChanges": "Apply Changes", "ApplyTags": "Apply Tags", - "ApplyTagsHelpTexts1": "How to apply tags to the selected indexers", - "ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags", - "ApplyTagsHelpTexts3": "Remove: Remove the entered tags", - "ApplyTagsHelpTexts4": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", + "ApplyTagsHelpTextHowToApply": "How to apply tags to the selected indexers", + "ApplyTagsHelpTextAdd": "Add: Add the tags the existing list of tags", + "ApplyTagsHelpTextRemove": "Remove: Remove the entered tags", + "ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", "AutoAdd": "Auto Add", "AutomaticAdd": "Automatic Add", "BlocklistRelease": "Blocklist Release", From ef6ff370baef4cc0fecdff714077d5ac3b8928d0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Jul 2023 09:24:24 -0700 Subject: [PATCH 04/18] Fixed: Translating static strings --- frontend/src/Utilities/String/translate.js | 11 +++++++++-- frontend/src/index.js | 12 +++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/Utilities/String/translate.js b/frontend/src/Utilities/String/translate.js index 802824524..c2dd96291 100644 --- a/frontend/src/Utilities/String/translate.js +++ b/frontend/src/Utilities/String/translate.js @@ -11,8 +11,15 @@ function getTranslations() { let translations = {}; export function fetchTranslations() { - return getTranslations().then((data) => { - translations = data.strings; + return new Promise(async(resolve) => { + try { + const data = await getTranslations(); + translations = data.strings; + + resolve(true); + } catch (error) { + resolve(false); + } }); } diff --git a/frontend/src/index.js b/frontend/src/index.js index e9284a7de..6472914f4 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,8 +3,8 @@ import React from 'react'; import { render } from 'react-dom'; import createAppStore from 'Store/createAppStore'; import { fetchTranslations } from 'Utilities/String/translate'; -import App from './App/App'; +// import App from './App/App'; import './preload'; import './polyfills'; import 'Diag/ConsoleApi'; @@ -13,14 +13,8 @@ import './index.css'; const history = createBrowserHistory(); const store = createAppStore(history); -let hasTranslationsError = false; - -try { - await fetchTranslations(); - -} catch (error) { - hasTranslationsError = true; -} +const hasTranslationsError = !await fetchTranslations(); +const { default: App } = await import('./App/App'); render( <App From eadd0c4e101af0386360bbff859f80e008a5be7c Mon Sep 17 00:00:00 2001 From: jack-mil <62065280+jack-mil@users.noreply.github.com> Date: Sat, 15 Jul 2023 13:28:38 -0400 Subject: [PATCH 05/18] New: Optionally show Custom Format Score for episodes on Series Details --- frontend/src/Series/Details/EpisodeRow.css | 6 +++++ .../src/Series/Details/EpisodeRow.css.d.ts | 1 + frontend/src/Series/Details/EpisodeRow.js | 23 +++++++++++++++++++ .../src/Series/Details/EpisodeRowConnector.js | 1 + frontend/src/Store/Actions/episodeActions.js | 14 ++++++++++- src/NzbDrone.Core/Localization/Core/en.json | 1 + .../EpisodeFiles/EpisodeFileResource.cs | 6 ++++- 7 files changed, 50 insertions(+), 2 deletions(-) diff --git a/frontend/src/Series/Details/EpisodeRow.css b/frontend/src/Series/Details/EpisodeRow.css index b27a69f1a..4a0940362 100644 --- a/frontend/src/Series/Details/EpisodeRow.css +++ b/frontend/src/Series/Details/EpisodeRow.css @@ -56,3 +56,9 @@ width: 120px; } + +.customFormatScore { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 55px; +} diff --git a/frontend/src/Series/Details/EpisodeRow.css.d.ts b/frontend/src/Series/Details/EpisodeRow.css.d.ts index 138000856..d4a5cfe93 100644 --- a/frontend/src/Series/Details/EpisodeRow.css.d.ts +++ b/frontend/src/Series/Details/EpisodeRow.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'audio': string; 'audioLanguages': string; + 'customFormatScore': string; 'episodeNumber': string; 'episodeNumberAnime': string; 'languages': string; diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js index eba223b16..4b9e827f6 100644 --- a/frontend/src/Series/Details/EpisodeRow.js +++ b/frontend/src/Series/Details/EpisodeRow.js @@ -4,6 +4,7 @@ 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 Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeNumber from 'Episode/EpisodeNumber'; import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; @@ -12,7 +13,9 @@ import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; +import { tooltipPositions } from 'Helpers/Props'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatRuntime from 'Utilities/Number/formatRuntime'; import styles from './EpisodeRow.css'; @@ -72,6 +75,7 @@ class EpisodeRow extends Component { episodeFileSize, releaseGroup, customFormats, + customFormatScore, alternateTitles, columns } = this.props; @@ -193,6 +197,24 @@ class EpisodeRow extends Component { ); } + if (name === 'customFormatScore') { + return ( + <TableRowCell + key={name} + className={styles.customFormatScore} + > + <Tooltip + anchor={formatPreferredWordScore( + customFormatScore, + customFormats.length + )} + tooltip={<EpisodeFormats formats={customFormats} />} + position={tooltipPositions.BOTTOM} + /> + </TableRowCell> + ); + } + if (name === 'languages') { return ( <TableRowCell @@ -355,6 +377,7 @@ EpisodeRow.propTypes = { episodeFileSize: PropTypes.number, releaseGroup: PropTypes.string, customFormats: PropTypes.arrayOf(PropTypes.object), + customFormatScore: PropTypes.number.isRequired, mediaInfo: PropTypes.object, alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Series/Details/EpisodeRowConnector.js b/frontend/src/Series/Details/EpisodeRowConnector.js index 6cfa67a4c..9559c289e 100644 --- a/frontend/src/Series/Details/EpisodeRowConnector.js +++ b/frontend/src/Series/Details/EpisodeRowConnector.js @@ -19,6 +19,7 @@ function createMapStateToProps() { episodeFileSize: episodeFile ? episodeFile.size : null, releaseGroup: episodeFile ? episodeFile.releaseGroup : null, customFormats: episodeFile ? episodeFile.customFormats : [], + customFormatScore: episodeFile ? episodeFile.customFormatScore : 0, alternateTitles: series.alternateTitles }; } diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js index 626bdeef0..b1860c089 100644 --- a/frontend/src/Store/Actions/episodeActions.js +++ b/frontend/src/Store/Actions/episodeActions.js @@ -1,10 +1,13 @@ import _ from 'lodash'; +import React from 'react'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; +import Icon from 'Components/Icon'; import episodeEntities from 'Episode/episodeEntities'; -import { sortDirections } from 'Helpers/Props'; +import { icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; +import translate from 'Utilities/String/translate'; import { updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; @@ -109,6 +112,15 @@ export const defaultState = { label: 'Formats', isVisible: false }, + { + name: 'customFormatScore', + columnLabel: translate('CustomFormatScore'), + label: React.createElement(Icon, { + name: icons.SCORE, + title: translate('CustomFormatScore') + }), + isVisible: false + }, { name: 'status', label: 'Status', diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index ebd7fcf13..e11957923 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -25,6 +25,7 @@ "CountImportListsSelected": "{count} import list(s) selected", "CountIndexersSelected": "{count} indexer(s) selected", "CountSeasons": "{count} seasons", + "CustomFormatScore": "Custom Format Score", "Delete": "Delete", "DeleteCondition": "Delete Condition", "DeleteConditionMessageText": "Are you sure you want to delete the condition '{0}'?", diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index 06c79cada..4d07eb117 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -24,6 +24,7 @@ namespace Sonarr.Api.V3.EpisodeFiles public List<Language> Languages { get; set; } public QualityModel Quality { get; set; } public List<CustomFormatResource> CustomFormats { get; set; } + public int CustomFormatScore { get; set; } public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -67,6 +68,8 @@ namespace Sonarr.Api.V3.EpisodeFiles } model.Series = series; + var customFormats = formatCalculationService?.ParseCustomFormat(model, model.Series); + var customFormatScore = series?.QualityProfile?.Value?.CalculateCustomFormatScore(customFormats) ?? 0; return new EpisodeFileResource { @@ -84,7 +87,8 @@ namespace Sonarr.Api.V3.EpisodeFiles Quality = model.Quality, MediaInfo = model.MediaInfo.ToResource(model.SceneName), QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality), - CustomFormats = formatCalculationService.ParseCustomFormat(model).ToResource(false) + CustomFormats = customFormats.ToResource(false), + CustomFormatScore = customFormatScore }; } } From d6aee683dc4e8a8c94993b30df21fe8f6c5106b3 Mon Sep 17 00:00:00 2001 From: Bogdan <mynameisbogdan@users.noreply.github.com> Date: Sat, 15 Jul 2023 20:29:47 +0300 Subject: [PATCH 06/18] New: Show tooltips with Custom Formats in History and Queue --- frontend/src/Activity/History/HistoryRow.js | 16 ++++++++++++++-- frontend/src/Activity/Queue/QueueRow.js | 13 +++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 9c005d246..32d73f204 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -4,13 +4,14 @@ import IconButton from 'Components/Link/IconButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; import episodeEntities from 'Episode/episodeEntities'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons } from 'Helpers/Props'; +import { icons, tooltipPositions } from 'Helpers/Props'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import HistoryDetailsModal from './Details/HistoryDetailsModal'; @@ -210,7 +211,14 @@ class HistoryRow extends Component { key={name} className={styles.customFormatScore} > - {formatPreferredWordScore(customFormatScore)} + <Tooltip + anchor={formatPreferredWordScore( + customFormatScore, + customFormats.length + )} + tooltip={<EpisodeFormats formats={customFormats} />} + position={tooltipPositions.BOTTOM} + /> </TableRowCell> ); } @@ -294,4 +302,8 @@ HistoryRow.propTypes = { onMarkAsFailedPress: PropTypes.func.isRequired }; +HistoryRow.defaultProps = { + customFormats: [] +}; + export default HistoryRow; diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 5334f69cf..708f960bb 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -8,12 +8,13 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableRow from 'Components/Table/TableRow'; +import Tooltip from 'Components/Tooltip/Tooltip'; import EpisodeFormats from 'Episode/EpisodeFormats'; import EpisodeLanguages from 'Episode/EpisodeLanguages'; import EpisodeQuality from 'Episode/EpisodeQuality'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons, kinds } from 'Helpers/Props'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -267,7 +268,14 @@ class QueueRow extends Component { key={name} className={styles.customFormatScore} > - {formatPreferredWordScore(customFormatScore)} + <Tooltip + anchor={formatPreferredWordScore( + customFormatScore, + customFormats.length + )} + tooltip={<EpisodeFormats formats={customFormats} />} + position={tooltipPositions.BOTTOM} + /> </TableRowCell> ); } @@ -450,6 +458,7 @@ QueueRow.propTypes = { }; QueueRow.defaultProps = { + customFormats: [], isGrabbing: false, isRemoving: false }; From 5b3b346b77a4fd6f4162c1e68e58fac3c284be1d Mon Sep 17 00:00:00 2001 From: Sonarr <development@sonarr.tv> Date: Sat, 15 Jul 2023 17:31:39 +0000 Subject: [PATCH 07/18] Automated API Docs update ignore-downstream --- src/Sonarr.Api.V3/openapi.json | 178 +++++++-------------------------- 1 file changed, 34 insertions(+), 144 deletions(-) diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index 20b2c7997..15432ae5a 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -4062,53 +4062,6 @@ } } }, - "/api/v3/metadata/bulk": { - "put": { - "tags": [ - "Metadata" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataBulkResource" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataResource" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Metadata" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataBulkResource" - } - } - } - }, - "responses": { - "200": { - "description": "Success" - } - } - } - }, "/api/v3/metadata/schema": { "get": { "tags": [ @@ -4673,53 +4626,6 @@ } } }, - "/api/v3/notification/bulk": { - "put": { - "tags": [ - "Notification" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationBulkResource" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationResource" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Notification" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationBulkResource" - } - } - } - }, - "responses": { - "200": { - "description": "Success" - } - } - } - }, "/api/v3/notification/schema": { "get": { "tags": [ @@ -7868,6 +7774,10 @@ }, "nullable": true }, + "customFormatScore": { + "type": "integer", + "format": "int32" + }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" }, @@ -9301,31 +9211,6 @@ }, "additionalProperties": false }, - "MetadataBulkResource": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "tags": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "applyTags": { - "$ref": "#/components/schemas/ApplyTags" - } - }, - "additionalProperties": false - }, "MetadataResource": { "type": "object", "properties": { @@ -9484,31 +9369,6 @@ }, "additionalProperties": false }, - "NotificationBulkResource": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "tags": { - "type": "array", - "items": { - "type": "integer", - "format": "int32" - }, - "nullable": true - }, - "applyTags": { - "$ref": "#/components/schemas/ApplyTags" - } - }, - "additionalProperties": false - }, "NotificationResource": { "type": "object", "properties": { @@ -9685,6 +9545,24 @@ "$ref": "#/components/schemas/EpisodeResource" }, "nullable": true + }, + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Language" + }, + "nullable": true + }, + "customFormats": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomFormatResource" + }, + "nullable": true + }, + "customFormatScore": { + "type": "integer", + "format": "int32" } }, "additionalProperties": false @@ -10089,6 +9967,10 @@ }, "nullable": true }, + "customFormatScore": { + "type": "integer", + "format": "int32" + }, "size": { "type": "number", "format": "double" @@ -11328,6 +11210,14 @@ }, "nullable": true }, + "downloadClientIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, "autoTagIds": { "type": "array", "items": { From 35e171f7b120ba68f4c2f5a7ada5ba204caf7f45 Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Jul 2023 14:34:31 -0700 Subject: [PATCH 08/18] Fixed: Order of Discord grab notification fields --- src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs index f51c6a343..d0d9e8860 100644 --- a/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordFieldType.cs @@ -12,9 +12,9 @@ namespace NzbDrone.Core.Notifications.Discord Release, Poster, Fanart, + Indexer, CustomFormats, - CustomFormatScore, - Indexer + CustomFormatScore } public enum DiscordImportFieldType From c206b9291231cc3eb75727aa2ec002fab7dfac9b Mon Sep 17 00:00:00 2001 From: Stevie Robinson <stevie.robinson@gmail.com> Date: Sat, 15 Jul 2023 23:37:31 +0200 Subject: [PATCH 09/18] Translate sidebar strings --- .../Components/Page/Sidebar/PageSidebar.js | 65 ++++++++++--------- src/NzbDrone.Core/Localization/Core/en.json | 30 +++++++++ 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index c7c6844a8..fcf4bc02f 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -10,6 +10,7 @@ import { icons } from 'Helpers/Props'; import locationShape from 'Helpers/Props/Shapes/locationShape'; import dimensions from 'Styles/Variables/dimensions'; import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import translate from 'Utilities/String/translate'; import MessagesConnector from './Messages/MessagesConnector'; import PageSidebarItem from './PageSidebarItem'; import styles from './PageSidebar.css'; @@ -20,16 +21,16 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); const links = [ { iconName: icons.SERIES_CONTINUING, - title: 'Series', + title: translate('Series'), to: '/', alias: '/series', children: [ { - title: 'Add New', + title: translate('AddNew'), to: '/add/new' }, { - title: 'Library Import', + title: translate('LibraryImport'), to: '/add/import' } ] @@ -37,26 +38,26 @@ const links = [ { iconName: icons.CALENDAR, - title: 'Calendar', + title: translate('Calendar'), to: '/calendar' }, { iconName: icons.ACTIVITY, - title: 'Activity', + title: translate('Activity'), to: '/activity/queue', children: [ { - title: 'Queue', + title: translate('Queue'), to: '/activity/queue', statusComponent: QueueStatusConnector }, { - title: 'History', + title: translate('History'), to: '/activity/history' }, { - title: 'Blocklist', + title: translate('Blocklist'), to: '/activity/blocklist' } ] @@ -64,15 +65,15 @@ const links = [ { iconName: icons.WARNING, - title: 'Wanted', + title: translate('Wanted'), to: '/wanted/missing', children: [ { - title: 'Missing', + title: translate('Missing'), to: '/wanted/missing' }, { - title: 'Cutoff Unmet', + title: translate('CutoffUnmet'), to: '/wanted/cutoffunmet' } ] @@ -80,59 +81,59 @@ const links = [ { iconName: icons.SETTINGS, - title: 'Settings', + title: translate('Settings'), to: '/settings', children: [ { - title: 'Media Management', + title: translate('MediaManagement'), to: '/settings/mediamanagement' }, { - title: 'Profiles', + title: translate('Profiles'), to: '/settings/profiles' }, { - title: 'Quality', + title: translate('Quality'), to: '/settings/quality' }, { - title: 'Custom Formats', + title: translate('CustomFormats'), to: '/settings/customformats' }, { - title: 'Indexers', + title: translate('Indexers'), to: '/settings/indexers' }, { - title: 'Download Clients', + title: translate('DownloadClients'), to: '/settings/downloadclients' }, { - title: 'Import Lists', + title: translate('ImportLists'), to: '/settings/importlists' }, { - title: 'Connect', + title: translate('Connect'), to: '/settings/connect' }, { - title: 'Metadata', + title: translate('Metadata'), to: '/settings/metadata' }, { - title: 'Metadata Source', + title: translate('MetadataSource'), to: '/settings/metadatasource' }, { - title: 'Tags', + title: translate('Tags'), to: '/settings/tags' }, { - title: 'General', + title: translate('General'), to: '/settings/general' }, { - title: 'UI', + title: translate('UI'), to: '/settings/ui' } ] @@ -140,32 +141,32 @@ const links = [ { iconName: icons.SYSTEM, - title: 'System', + title: translate('System'), to: '/system/status', children: [ { - title: 'Status', + title: translate('Status'), to: '/system/status', statusComponent: HealthStatusConnector }, { - title: 'Tasks', + title: translate('Tasks'), to: '/system/tasks' }, { - title: 'Backup', + title: translate('Backup'), to: '/system/backup' }, { - title: 'Updates', + title: translate('Updates'), to: '/system/updates' }, { - title: 'Events', + title: translate('Events'), to: '/system/events' }, { - title: 'Log Files', + title: translate('LogFiles'), to: '/system/logs/files' } ] diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index e11957923..3afbcaa35 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1,7 +1,9 @@ { + "Activity": "Activity", "Add": "Add", "Added": "Added", "AddingTag": "Adding tag", + "AddNew": "Add New", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "Apply": "Apply", @@ -13,19 +15,25 @@ "ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", "AutoAdd": "Auto Add", "AutomaticAdd": "Automatic Add", + "Backup": "Backup", + "Blocklist": "Blocklist", "BlocklistRelease": "Blocklist Release", "BlocklistReleaseHelpText": "Prevents Sonarr from automatically grabbing this release again", "BlocklistReleases": "Blocklist Releases", "Browser Reload Required": "Browser Reload Required", + "Calendar": "Calendar", "Cancel": "Cancel", "CloneCondition": "Clone Condition", "CloneCustomFormat": "Clone Custom Format", "Close": "Close", + "Connect": "Connect", "CountDownloadClientsSelected": "{count} download client(s) selected", "CountImportListsSelected": "{count} import list(s) selected", "CountIndexersSelected": "{count} indexer(s) selected", "CountSeasons": "{count} seasons", "CustomFormatScore": "Custom Format Score", + "CustomFormats": "Custom Formats", + "CutoffUnmet": "Cutoff Unmet", "Delete": "Delete", "DeleteCondition": "Delete Condition", "DeleteConditionMessageText": "Are you sure you want to delete the condition '{0}'?", @@ -38,6 +46,7 @@ "DeleteSelectedIndexers": "Delete Indexer(s)", "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {count} selected indexer(s)?", "Disabled": "Disabled", + "DownloadClients": "Download Clients", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {0}.", "DownloadClientRootFolderHealthCheckMessage": "Download client {0} places downloads in the root folder {1}. You should not download to a root folder.", @@ -54,11 +63,15 @@ "EnableRSS": "Enable RSS", "Enabled": "Enabled", "Ended": "Ended", + "Events": "Events", "ExistingTag": "Existing tag", "ExportCustomFormat": "Export Custom Format", + "General": "General", "HiddenClickToShow": "Hidden, click to show", "HideAdvanced": "Hide Advanced", + "History": "History", "Implementation": "Implementation", + "ImportLists": "Import Lists", "ImportListRootFolderMissingRootHealthCheckMessage": "Missing root folder for import list(s): {0}", "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Multiple root folders are missing for import lists: {0}", "ImportListStatusAllUnavailableHealthCheckMessage": "All lists are unavailable due to failures", @@ -66,6 +79,7 @@ "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Enable Completed Download Handling if possible", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling", + "Indexers": "Indexers", "IndexerJackettAllHealthCheckMessage": "Indexers using the unsupported Jackett 'all' endpoint: {0}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures for more than 6 hours", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures for more than 6 hours: {0}", @@ -78,11 +92,17 @@ "IndexerStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures: {0}", "Language": "Language", "Language that Sonarr will use for UI": "Language that Sonarr will use for UI", + "LibraryImport": "Library Import", + "LogFiles": "Log Files", "ManageClients": "Manage Clients", "ManageDownloadClients": "Manage Download Clients", "ManageImportLists": "Manage Import Lists", "ManageIndexers": "Manage Indexers", "ManageLists": "Manage Lists", + "MediaManagement": "Media Management", + "Metadata": "Metadata", + "MetadataSource": "Metadata Source", + "Missing": "Missing", "Monitored": "Monitored", "MountHealthCheckMessage": "Mount containing a series path is mounted read-only: ", "Name": "Name", @@ -100,10 +120,13 @@ "Path": "Path", "PreviousAiring": "Previous Airing", "Priority": "Priority", + "Profiles": "Profiles", "ProxyBadRequestHealthCheckMessage": "Failed to test proxy. Status Code: {0}", "ProxyFailedToTestHealthCheckMessage": "Failed to test proxy: {0}", "ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}", + "Quality": "Quality", "QualityProfile": "Quality Profile", + "Queue": "Queue", "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {0}. Ensure this path exists and is writable by the user running Sonarr", "RefreshSeries": "Refresh Series", "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {0} places downloads in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", @@ -142,17 +165,24 @@ "RootFolderMissingHealthCheckMessage": "Missing root folder: {0}", "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {0}", "SearchForMonitoredEpisodes": "Search for monitored episodes", + "Series": "Series", "SetTags": "Set Tags", + "Settings": "Settings", "ShowAdvanced": "Show Advanced", "ShownClickToHide": "Shown, click to hide", "SizeOnDisk": "Size on disk", + "System": "System", "SystemTimeHealthCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", "Tags": "Tags", + "Tasks": "Tasks", + "UI": "UI", "UI Language": "UI Language", "Unmonitored": "Unmonitored", + "Updates": "Updates", "UpdateAvailableHealthCheckMessage": "New update is available", "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.", "UpdateUINotWritableHealthCheckMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", + "Wanted": "Wanted", "Yes": "Yes" } From f62bc59a731a76734e0da9218ea1d90198e07a6d Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Sat, 15 Jul 2023 21:12:51 -0700 Subject: [PATCH 10/18] Fixed: Translating column names --- frontend/src/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/index.js b/frontend/src/index.js index 6472914f4..cb9240c7e 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,10 +1,8 @@ import { createBrowserHistory } from 'history'; import React from 'react'; import { render } from 'react-dom'; -import createAppStore from 'Store/createAppStore'; import { fetchTranslations } from 'Utilities/String/translate'; -// import App from './App/App'; import './preload'; import './polyfills'; import 'Diag/ConsoleApi'; @@ -12,10 +10,13 @@ import 'Styles/globals.css'; import './index.css'; const history = createBrowserHistory(); -const store = createAppStore(history); const hasTranslationsError = !await fetchTranslations(); + +const { default: createAppStore } = await import('Store/createAppStore'); const { default: App } = await import('./App/App'); +const store = createAppStore(history); + render( <App store={store} From f0cb5b81f140c67fa84162e094cc4e0f3476f5da Mon Sep 17 00:00:00 2001 From: Mark McDowall <mark@mcdowall.ca> Date: Mon, 17 Jul 2023 20:37:02 -0700 Subject: [PATCH 11/18] UI loading improvements Fixed: Caching for dynamically loaded JS files Fixed: Incorrect caching of initialize.js --- frontend/build/webpack.config.js | 15 ++--- frontend/src/bootstrap.tsx | 23 +++++++ frontend/src/index.ejs | 13 ++-- frontend/src/index.js | 27 -------- frontend/src/index.ts | 19 ++++++ frontend/src/preload.js | 2 - frontend/tsconfig.json | 4 +- package.json | 2 +- .../Frontend/InitializeJsController.cs | 65 ------------------- .../Frontend/InitializeJsonController.cs | 65 +++++++++++++++++++ .../Frontend/Mappers/HtmlMapperBase.cs | 4 +- .../Frontend/Mappers/StaticResourceMapper.cs | 2 +- .../Middleware/CacheableSpecification.cs | 2 +- 13 files changed, 130 insertions(+), 113 deletions(-) create mode 100644 frontend/src/bootstrap.tsx delete mode 100644 frontend/src/index.js create mode 100644 frontend/src/index.ts delete mode 100644 frontend/src/preload.js delete mode 100644 src/Sonarr.Http/Frontend/InitializeJsController.cs create mode 100644 src/Sonarr.Http/Frontend/InitializeJsonController.cs diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 9201a5e76..dc2a575a8 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -36,7 +36,7 @@ module.exports = (env) => { }, entry: { - index: 'index.js' + index: 'index.ts' }, resolve: { @@ -67,17 +67,13 @@ module.exports = (env) => { output: { path: distFolder, publicPath: '/', - filename: '[name].js', - sourceMapFilename: '[file].map' + filename: '[name].[contenthash].js', + sourceMapFilename: '[file]-[contenthash].map' }, optimization: { moduleIds: 'deterministic', - chunkIds: 'named', - splitChunks: { - chunks: 'initial', - name: 'vendors' - } + chunkIds: 'named' }, performance: { @@ -101,7 +97,8 @@ module.exports = (env) => { new HtmlWebpackPlugin({ template: 'frontend/src/index.ejs', filename: 'index.html', - publicPath: '/' + publicPath: '/', + inject: false }), new FileManagerPlugin({ diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx new file mode 100644 index 000000000..a729cb3c5 --- /dev/null +++ b/frontend/src/bootstrap.tsx @@ -0,0 +1,23 @@ +import { createBrowserHistory } from 'history'; +import React from 'react'; +import { render } from 'react-dom'; +import createAppStore from 'Store/createAppStore'; +import { fetchTranslations } from 'Utilities/String/translate'; +import App from './App/App'; + +import 'Diag/ConsoleApi'; + +export async function bootstrap() { + const history = createBrowserHistory(); + const store = createAppStore(history); + const hasTranslationsError = !(await fetchTranslations()); + + render( + <App + store={store} + history={history} + hasTranslationsError={hasTranslationsError} + />, + document.getElementById('root') + ); +} diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 712a2d77f..97a0104ee 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -48,7 +48,15 @@ /> <link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css"> - <!-- webpack bundles head --> + + <script> + window.Sonarr = { + urlBase: '__URL_BASE__' + }; + </script> + + <% for (key in htmlWebpackPlugin.files.js) { %><script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[key] %>" data-no-hash></script><% } %> + <% for (key in htmlWebpackPlugin.files.css) { %><link href="<%= htmlWebpackPlugin.files.css[key] %>" rel="stylesheet"></link><% } %> <title>Sonarr @@ -77,7 +85,4 @@
- - - diff --git a/frontend/src/index.js b/frontend/src/index.js deleted file mode 100644 index cb9240c7e..000000000 --- a/frontend/src/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import { createBrowserHistory } from 'history'; -import React from 'react'; -import { render } from 'react-dom'; -import { fetchTranslations } from 'Utilities/String/translate'; - -import './preload'; -import './polyfills'; -import 'Diag/ConsoleApi'; -import 'Styles/globals.css'; -import './index.css'; - -const history = createBrowserHistory(); -const hasTranslationsError = !await fetchTranslations(); - -const { default: createAppStore } = await import('Store/createAppStore'); -const { default: App } = await import('./App/App'); - -const store = createAppStore(history); - -render( - , - document.getElementById('root') -); diff --git a/frontend/src/index.ts b/frontend/src/index.ts new file mode 100644 index 000000000..bbb3b5932 --- /dev/null +++ b/frontend/src/index.ts @@ -0,0 +1,19 @@ +import './polyfills'; +import 'Styles/globals.css'; +import './index.css'; + +const initializeUrl = `${ + window.Sonarr.urlBase +}/initialize.json?t=${Date.now()}`; +const response = await fetch(initializeUrl); + +window.Sonarr = await response.json(); + +/* eslint-disable no-undef, @typescript-eslint/ban-ts-comment */ +// @ts-ignore 2304 +__webpack_public_path__ = `${window.Sonarr.urlBase}/`; +/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ + +const { bootstrap } = await import('./bootstrap'); + +await bootstrap(); diff --git a/frontend/src/preload.js b/frontend/src/preload.js deleted file mode 100644 index e74b4f1be..000000000 --- a/frontend/src/preload.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint no-undef: 0 */ -__webpack_public_path__ = `${window.Sonarr.urlBase}/`; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 354e2a5aa..611c872ed 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "es6", + "target": "esnext", "allowJs": true, "checkJs": false, "baseUrl": "src", "jsx": "react", - "module": "commonjs", + "module": "esnext", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, diff --git a/package.json b/package.json index 9a88c0f42..1a5d8ebec 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "author": "Team Sonarr", "license": "GPL-3.0", "readmeFilename": "readme.md", - "main": "index.js", + "main": "index.ts", "browserslist": [ "defaults" ], diff --git a/src/Sonarr.Http/Frontend/InitializeJsController.cs b/src/Sonarr.Http/Frontend/InitializeJsController.cs deleted file mode 100644 index b96428c18..000000000 --- a/src/Sonarr.Http/Frontend/InitializeJsController.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Text; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using NzbDrone.Common; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Analytics; -using NzbDrone.Core.Configuration; - -namespace Sonarr.Http.Frontend -{ - [Authorize(Policy = "UI")] - [ApiController] - public class InitializeJsController : Controller - { - private readonly IConfigFileProvider _configFileProvider; - private readonly IAnalyticsService _analyticsService; - - private static string _apiKey; - private static string _urlBase; - private string _generatedContent; - - public InitializeJsController(IConfigFileProvider configFileProvider, - IAnalyticsService analyticsService) - { - _configFileProvider = configFileProvider; - _analyticsService = analyticsService; - - _apiKey = configFileProvider.ApiKey; - _urlBase = configFileProvider.UrlBase; - } - - [HttpGet("/initialize.js")] - public IActionResult Index() - { - return Content(GetContent(), "application/javascript"); - } - - private string GetContent() - { - if (RuntimeInfo.IsProduction && _generatedContent != null) - { - return _generatedContent; - } - - var builder = new StringBuilder(); - builder.AppendLine("window.Sonarr = {"); - builder.AppendLine($" apiRoot: '{_urlBase}/api/v3',"); - builder.AppendLine($" apiKey: '{_apiKey}',"); - builder.AppendLine($" release: '{BuildInfo.Release}',"); - builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',"); - builder.AppendLine($" instanceName: '{_configFileProvider.InstanceName.ToString()}',"); - builder.AppendLine($" theme: '{_configFileProvider.Theme.ToString()}',"); - builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',"); - builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); - builder.AppendLine($" userHash: '{HashUtil.AnonymousToken()}',"); - builder.AppendLine($" urlBase: '{_urlBase}',"); - builder.AppendLine($" isProduction: {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); - builder.AppendLine("};"); - - _generatedContent = builder.ToString(); - - return _generatedContent; - } - } -} diff --git a/src/Sonarr.Http/Frontend/InitializeJsonController.cs b/src/Sonarr.Http/Frontend/InitializeJsonController.cs new file mode 100644 index 000000000..903f6c44f --- /dev/null +++ b/src/Sonarr.Http/Frontend/InitializeJsonController.cs @@ -0,0 +1,65 @@ +using System.Text; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Analytics; +using NzbDrone.Core.Configuration; + +namespace Sonarr.Http.Frontend +{ + [Authorize(Policy = "UI")] + [ApiController] + public class InitializeJsonController : Controller + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IAnalyticsService _analyticsService; + + private static string _apiKey; + private static string _urlBase; + private string _generatedContent; + + public InitializeJsonController(IConfigFileProvider configFileProvider, + IAnalyticsService analyticsService) + { + _configFileProvider = configFileProvider; + _analyticsService = analyticsService; + + _apiKey = configFileProvider.ApiKey; + _urlBase = configFileProvider.UrlBase; + } + + [HttpGet("/initialize.json")] + public IActionResult Index() + { + return Content(GetContent(), "application/json"); + } + + private string GetContent() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var builder = new StringBuilder(); + builder.AppendLine("{"); + builder.AppendLine($" \"apiRoot\": \"{_urlBase}/api/v3\","); + builder.AppendLine($" \"apiKey\": \"{_apiKey}\","); + builder.AppendLine($" \"release\": \"{BuildInfo.Release}\","); + builder.AppendLine($" \"version\": \"{BuildInfo.Version.ToString()}\","); + builder.AppendLine($" \"instanceName\": \"{_configFileProvider.InstanceName.ToString()}\","); + builder.AppendLine($" \"theme\": \"{_configFileProvider.Theme.ToString()}\","); + builder.AppendLine($" \"branch\": \"{_configFileProvider.Branch.ToLower()}\","); + builder.AppendLine($" \"analytics\": {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},"); + builder.AppendLine($" \"userHash\": \"{HashUtil.AnonymousToken()}\","); + builder.AppendLine($" \"urlBase\": \"{_urlBase}\","); + builder.AppendLine($" \"isProduction\": {RuntimeInfo.IsProduction.ToString().ToLowerInvariant()}"); + builder.AppendLine("}"); + + _generatedContent = builder.ToString(); + + return _generatedContent; + } + } +} diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs index f593edebd..6dcd25b16 100644 --- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -62,9 +62,11 @@ namespace Sonarr.Http.Frontend.Mappers url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value); } - return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, UrlBase, url); + return $"{match.Groups["attribute"].Value}=\"{UrlBase}{url}\""; }); + text = text.Replace("__URL_BASE__", UrlBase); + _generatedContent = text; return _generatedContent; diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 205a1383d..32b8ecb42 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -37,7 +37,7 @@ namespace Sonarr.Http.Frontend.Mappers } return resourceUrl.StartsWith("/content") || - (resourceUrl.EndsWith(".js") && !resourceUrl.EndsWith("initialize.js")) || + resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".map") || resourceUrl.EndsWith(".css") || (resourceUrl.EndsWith(".ico") && !resourceUrl.Equals("/favicon.ico")) || diff --git a/src/Sonarr.Http/Middleware/CacheableSpecification.cs b/src/Sonarr.Http/Middleware/CacheableSpecification.cs index 586e38a86..1bb9138aa 100644 --- a/src/Sonarr.Http/Middleware/CacheableSpecification.cs +++ b/src/Sonarr.Http/Middleware/CacheableSpecification.cs @@ -46,7 +46,7 @@ namespace Sonarr.Http.Middleware return false; } - if (path.EndsWith("/initialize.js")) + if (path.EndsWith("/initialize.json")) { return false; } From bc374f07cebc8463c6416630d0fb06f011f21c31 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 14 Jul 2023 06:49:48 +0300 Subject: [PATCH 12/18] Use 2 spaces indentation for ts/tsx files --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 8b577f8d4..d91f68d99 100644 --- a/.editorconfig +++ b/.editorconfig @@ -268,7 +268,7 @@ dotnet_diagnostic.CA5397.severity = suggestion dotnet_diagnostic.SYSLIB0006.severity = none -[*.{js,html,js,hbs,less,css}] +[*.{js,html,hbs,less,css,ts,tsx}] charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true From 67234222e39b7bbf0662f8391e9ab486ecefa1b7 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 14 Jul 2023 06:59:02 +0300 Subject: [PATCH 13/18] Fixed: Improve quality and episode info output in parse result --- frontend/src/Parse/ParseModalContent.tsx | 5 +- frontend/src/Parse/ParseResult.css | 20 +-- frontend/src/Parse/ParseResult.css.d.ts | 5 +- frontend/src/Parse/ParseResult.tsx | 179 ++++++++++---------- frontend/src/Parse/ParseToolbarButton.tsx | 3 +- src/NzbDrone.Core/Localization/Core/en.json | 38 ++++- 6 files changed, 137 insertions(+), 113 deletions(-) diff --git a/frontend/src/Parse/ParseModalContent.tsx b/frontend/src/Parse/ParseModalContent.tsx index 2e788cd35..cdff08376 100644 --- a/frontend/src/Parse/ParseModalContent.tsx +++ b/frontend/src/Parse/ParseModalContent.tsx @@ -11,6 +11,7 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import { icons } from 'Helpers/Props'; import { clear, fetch } from 'Store/Actions/parseActions'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import translate from 'Utilities/String/translate'; import ParseResult from './ParseResult'; import parseStateSelector from './parseStateSelector'; import styles from './ParseModalContent.css'; @@ -58,7 +59,7 @@ function ParseModalContent(props: ParseModalContentProps) { return ( - Test Parsing + {translate('TestParsing')}
@@ -115,7 +116,7 @@ function ParseModalContent(props: ParseModalContentProps) { - + ); diff --git a/frontend/src/Parse/ParseResult.css b/frontend/src/Parse/ParseResult.css index d5de120fa..c49c4e3fa 100644 --- a/frontend/src/Parse/ParseResult.css +++ b/frontend/src/Parse/ParseResult.css @@ -1,20 +1,8 @@ -.item { +.container { display: flex; + flex-wrap: wrap; } -.title { - margin-right: 20px; - width: 250px; - text-align: right; - font-weight: bold; -} - -.description { - /* composes: description from '~Components/DescriptionList/DescriptionListItemTitle.css'; */ -} - -@media (max-width: $breakpointSmall) { - .item { - display: block; - } +.column { + flex: 0 0 50%; } diff --git a/frontend/src/Parse/ParseResult.css.d.ts b/frontend/src/Parse/ParseResult.css.d.ts index 13942714e..653368e06 100644 --- a/frontend/src/Parse/ParseResult.css.d.ts +++ b/frontend/src/Parse/ParseResult.css.d.ts @@ -1,9 +1,8 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { - 'description': string; - 'item': string; - 'title': string; + 'column': string; + 'container': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Parse/ParseResult.tsx b/frontend/src/Parse/ParseResult.tsx index 575b8467f..e5dafc240 100644 --- a/frontend/src/Parse/ParseResult.tsx +++ b/frontend/src/Parse/ParseResult.tsx @@ -5,6 +5,7 @@ import EpisodeFormats from 'Episode/EpisodeFormats'; import SeriesTitleLink from 'Series/SeriesTitleLink'; import translate from 'Utilities/String/translate'; import ParseResultItem from './ParseResultItem'; +import styles from './ParseResult.css'; interface ParseResultProps { item: ParseModel; @@ -45,11 +46,11 @@ function ParseResult(props: ParseResultProps) {
- + 0 ? seriesTitleInfo.allTitles.join(', ') @@ -66,105 +67,113 @@ function ParseResult(props: ParseResultProps) { />
- {/* - - Year - Secondary titles - special episode - - */} +
+
+
+ -
- + - + - + - + +
- +
+ - + - + - - - + +
+
- +
+
+ + 1 && !quality.revision.isRepack + ? 'True' + : '-' + } + /> - 1 ? quality.revision.version : '-'} - /> + +
- +
+ 1 ? quality.revision.version : '-' + } + /> - 1 && !quality.revision.isRepack - ? 'True' - : '-' - } - /> - - + +
+
@@ -176,7 +185,7 @@ function ParseResult(props: ParseResultProps) {
@@ -218,12 +227,12 @@ function ParseResult(props: ParseResultProps) { /> } />
diff --git a/frontend/src/Parse/ParseToolbarButton.tsx b/frontend/src/Parse/ParseToolbarButton.tsx index 66724d852..43b8b959f 100644 --- a/frontend/src/Parse/ParseToolbarButton.tsx +++ b/frontend/src/Parse/ParseToolbarButton.tsx @@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useState } from 'react'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import { icons } from 'Helpers/Props'; import ParseModal from 'Parse/ParseModal'; +import translate from 'Utilities/String/translate'; function ParseToolbarButton() { const [isParseModalOpen, setIsParseModalOpen] = useState(false); @@ -17,7 +18,7 @@ function ParseToolbarButton() { return ( diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 3afbcaa35..460213e06 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1,16 +1,19 @@ { + "AbsoluteEpisodeNumbers": "Absolute Episode Number(s)", "Activity": "Activity", "Add": "Add", + "AddNew": "Add New", "Added": "Added", "AddingTag": "Adding tag", - "AddNew": "Add New", + "AirDate": "Air Date", + "AllTitles": "All Titles", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "Apply": "Apply", "ApplyChanges": "Apply Changes", "ApplyTags": "Apply Tags", - "ApplyTagsHelpTextHowToApply": "How to apply tags to the selected indexers", "ApplyTagsHelpTextAdd": "Add: Add the tags the existing list of tags", + "ApplyTagsHelpTextHowToApply": "How to apply tags to the selected indexers", "ApplyTagsHelpTextRemove": "Remove: Remove the entered tags", "ApplyTagsHelpTextReplace": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)", "AutoAdd": "Auto Add", @@ -34,6 +37,7 @@ "CustomFormatScore": "Custom Format Score", "CustomFormats": "Custom Formats", "CutoffUnmet": "Cutoff Unmet", + "Daily": "Daily", "Delete": "Delete", "DeleteCondition": "Delete Condition", "DeleteConditionMessageText": "Are you sure you want to delete the condition '{0}'?", @@ -45,14 +49,15 @@ "DeleteSelectedImportListsMessageText": "Are you sure you want to delete {count} selected import list(s)?", "DeleteSelectedIndexers": "Delete Indexer(s)", "DeleteSelectedIndexersMessageText": "Are you sure you want to delete {count} selected indexer(s)?", + "Details": "Details", "Disabled": "Disabled", - "DownloadClients": "Download Clients", "DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {0}.", "DownloadClientRootFolderHealthCheckMessage": "Download client {0} places downloads in the root folder {1}. You should not download to a root folder.", "DownloadClientSortingHealthCheckMessage": "Download client {0} has {1} sorting enabled for Sonarr's category. You should disable sorting in your download client to avoid import issues.", "DownloadClientStatusAllClientHealthCheckMessage": "All download clients are unavailable due to failures", "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {0}", + "DownloadClients": "Download Clients", "Edit": "Edit", "EditSelectedDownloadClients": "Edit Selected Download Clients", "EditSelectedImportLists": "Edit Selected Import Lists", @@ -63,23 +68,25 @@ "EnableRSS": "Enable RSS", "Enabled": "Enabled", "Ended": "Ended", + "EpisodeInfo": "Episode Info", + "EpisodeNumbers": "Episode Number(s)", "Events": "Events", "ExistingTag": "Existing tag", "ExportCustomFormat": "Export Custom Format", + "FullSeason": "Full Season", "General": "General", "HiddenClickToShow": "Hidden, click to show", "HideAdvanced": "Hide Advanced", "History": "History", "Implementation": "Implementation", - "ImportLists": "Import Lists", "ImportListRootFolderMissingRootHealthCheckMessage": "Missing root folder for import list(s): {0}", "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Multiple root folders are missing for import lists: {0}", "ImportListStatusAllUnavailableHealthCheckMessage": "All lists are unavailable due to failures", "ImportListStatusUnavailableHealthCheckMessage": "Lists unavailable due to failures: {0}", + "ImportLists": "Import Lists", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Enable Completed Download Handling if possible", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Enable Completed Download Handling", - "Indexers": "Indexers", "IndexerJackettAllHealthCheckMessage": "Indexers using the unsupported Jackett 'all' endpoint: {0}", "IndexerLongTermStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures for more than 6 hours", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures for more than 6 hours: {0}", @@ -90,8 +97,10 @@ "IndexerSearchNoInteractiveHealthCheckMessage": "No indexers available with Interactive Search enabled, Sonarr will not provide any interactive search results", "IndexerStatusAllUnavailableHealthCheckMessage": "All indexers are unavailable due to failures", "IndexerStatusUnavailableHealthCheckMessage": "Indexers unavailable due to failures: {0}", + "Indexers": "Indexers", "Language": "Language", "Language that Sonarr will use for UI": "Language that Sonarr will use for UI", + "Languages": "Languages", "LibraryImport": "Library Import", "LogFiles": "Log Files", "ManageClients": "Manage Clients", @@ -99,12 +108,16 @@ "ManageImportLists": "Manage Import Lists", "ManageIndexers": "Manage Indexers", "ManageLists": "Manage Lists", + "MatchedToEpisodes": "Matched to Episodes", + "MatchedToSeason": "Matched to Season", + "MatchedToSeries": "Matched to Series", "MediaManagement": "Media Management", "Metadata": "Metadata", "MetadataSource": "Metadata Source", "Missing": "Missing", "Monitored": "Monitored", "MountHealthCheckMessage": "Mount containing a series path is mounted read-only: ", + "MultiSeason": "Multi-Season", "Name": "Name", "Negated": "Negated", "Network": "Network", @@ -117,18 +130,25 @@ "NoSeasons": "No seasons", "OneSeason": "1 season", "OriginalLanguage": "Original Language", + "PartialSeason": "Partial Season", "Path": "Path", "PreviousAiring": "Previous Airing", "Priority": "Priority", "Profiles": "Profiles", + "Proper": "Proper", "ProxyBadRequestHealthCheckMessage": "Failed to test proxy. Status Code: {0}", "ProxyFailedToTestHealthCheckMessage": "Failed to test proxy: {0}", "ProxyResolveIpHealthCheckMessage": "Failed to resolve the IP Address for the Configured Proxy Host {0}", "Quality": "Quality", "QualityProfile": "Quality Profile", "Queue": "Queue", + "Real": "Real", "RecycleBinUnableToWriteHealthCheckMessage": "Unable to write to configured recycling bin folder: {0}. Ensure this path exists and is writable by the user running Sonarr", "RefreshSeries": "Refresh Series", + "Release": "Release", + "ReleaseGroup": "Release Group", + "ReleaseHash": "Release Hash", + "ReleaseTitle": "Release Title", "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {0} places downloads in {1} but this is not a valid {2} path. Review your remote path mappings and download client settings.", "RemotePathMappingDockerFolderMissingHealthCheckMessage": "You are using docker; download client {0} places downloads in {1} but this directory does not appear to exist inside the container. Review your remote path mappings and container volume settings.", "RemotePathMappingDownloadPermissionsHealthCheckMessage": "Sonarr can see but not access downloaded episode {0}. Likely permissions error.", @@ -158,6 +178,7 @@ "RemovedSeriesMultipleRemovedHealthCheckMessage": "Series {0} were removed from TheTVDB", "RemovedSeriesSingleRemovedHealthCheckMessage": "Series {0} was removed from TheTVDB", "RemovingTag": "Removing tag", + "Repack": "Repack", "Replace": "Replace", "Required": "Required", "Result": "Result", @@ -165,24 +186,29 @@ "RootFolderMissingHealthCheckMessage": "Missing root folder: {0}", "RootFolderMultipleMissingHealthCheckMessage": "Multiple root folders are missing: {0}", "SearchForMonitoredEpisodes": "Search for monitored episodes", + "SeasonNumber": "Season Number", "Series": "Series", + "SeriesTitle": "Series Title", "SetTags": "Set Tags", "Settings": "Settings", "ShowAdvanced": "Show Advanced", "ShownClickToHide": "Shown, click to hide", "SizeOnDisk": "Size on disk", + "Special": "Special", "System": "System", "SystemTimeHealthCheckMessage": "System time is off by more than 1 day. Scheduled tasks may not run correctly until the time is corrected", "Tags": "Tags", "Tasks": "Tasks", + "TestParsing": "Test Parsing", "UI": "UI", "UI Language": "UI Language", "Unmonitored": "Unmonitored", - "Updates": "Updates", "UpdateAvailableHealthCheckMessage": "New update is available", "UpdateStartupNotWritableHealthCheckMessage": "Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", "UpdateStartupTranslocationHealthCheckMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.", "UpdateUINotWritableHealthCheckMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", + "Updates": "Updates", + "Version": "Version", "Wanted": "Wanted", "Yes": "Yes" } From 5e19478266b33905e88b2e769269e44e5dd98e4b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 18 Jul 2023 06:45:46 +0300 Subject: [PATCH 14/18] Fixed: Error when selecting different Quality Profile --- .../src/Components/Form/QualityProfileSelectInputConnector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 7f305e65c..2304a0d67 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -69,7 +69,7 @@ class QualityProfileSelectInputConnector extends Component { // Listeners onChange = ({ name, value }) => { - this.props.onChange({ name, value: parseInt(value) }); + this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) }); }; // From 972e1408993fc4656196087c6646f23d222e41f5 Mon Sep 17 00:00:00 2001 From: jack-mil <62065280+jack-mil@users.noreply.github.com> Date: Mon, 17 Jul 2023 23:46:36 -0400 Subject: [PATCH 15/18] New: Show Custom Format score in Manual Import --- .../Interactive/InteractiveImportRow.tsx | 8 ++++++-- frontend/src/Store/Actions/interactiveImportActions.js | 4 ++++ src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs | 7 ++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index 763d697cb..dd740afef 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -30,6 +30,8 @@ import { import { SelectStateInputProps } from 'typings/props'; import Rejection from 'typings/Rejection'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; +import translate from 'Utilities/String/translate'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; import styles from './InteractiveImportRow.css'; @@ -57,6 +59,7 @@ interface InteractiveImportRowProps { languages?: Language[]; size: number; customFormats?: object[]; + customFormatScore?: number; rejections: Rejection[]; columns: Column[]; episodeFileId?: number; @@ -80,6 +83,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { releaseGroup, size, customFormats, + customFormatScore, rejections, isReprocessing, isSelected, @@ -427,8 +431,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { {customFormats?.length ? ( } - title="Formats" + anchor={formatPreferredWordScore(customFormatScore)} + title={translate('CustomFormats')} body={
diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index aae5e0e7b..28ad0f220 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -47,6 +47,10 @@ export const defaultState = { quality: function(item, direction) { return item.qualityWeight || 0; + }, + + customFormats: function(item, direction) { + return item.customFormatScore; } } }; diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index 28e34516b..590a66333 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -29,6 +29,7 @@ namespace Sonarr.Api.V3.ManualImport public int QualityWeight { get; set; } public string DownloadId { get; set; } public List CustomFormats { get; set; } + public int CustomFormatScore { get; set; } public IEnumerable Rejections { get; set; } } @@ -41,6 +42,9 @@ namespace Sonarr.Api.V3.ManualImport return null; } + var customFormats = model.CustomFormats; + var customFormatScore = model.Series?.QualityProfile?.Value?.CalculateCustomFormatScore(customFormats) ?? 0; + return new ManualImportResource { Id = HashConverter.GetHashInt31(model.Path), @@ -56,7 +60,8 @@ namespace Sonarr.Api.V3.ManualImport ReleaseGroup = model.ReleaseGroup, Quality = model.Quality, Languages = model.Languages, - CustomFormats = model.CustomFormats.ToResource(false), + CustomFormats = customFormats.ToResource(false), + CustomFormatScore = customFormatScore, // QualityWeight DownloadId = model.DownloadId, From 3ba7e64cd083618088e093baef8df7491ed1d9fb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 17 Jul 2023 21:05:17 -0700 Subject: [PATCH 16/18] Don't generate API docs for InitializeJson --- src/Sonarr.Http/Frontend/InitializeJsonController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sonarr.Http/Frontend/InitializeJsonController.cs b/src/Sonarr.Http/Frontend/InitializeJsonController.cs index 903f6c44f..57c4c31e3 100644 --- a/src/Sonarr.Http/Frontend/InitializeJsonController.cs +++ b/src/Sonarr.Http/Frontend/InitializeJsonController.cs @@ -10,6 +10,7 @@ namespace Sonarr.Http.Frontend { [Authorize(Policy = "UI")] [ApiController] + [ApiExplorerSettings(IgnoreApi = true)] public class InitializeJsonController : Controller { private readonly IConfigFileProvider _configFileProvider; From 87021fff434b52455442327b164a40d332755435 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 18 Jul 2023 07:07:08 +0300 Subject: [PATCH 17/18] Update translations for How To Apply Tags --- .../Index/Select/Tags/TagsModalContent.tsx | 29 +++++++++++-------- .../Manage/Tags/TagsModalContent.tsx | 2 +- .../Manage/Tags/TagsModalContent.tsx | 2 +- .../Indexers/Manage/Tags/TagsModalContent.tsx | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 4 +++ 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx index f0f6d28ae..434701ee7 100644 --- a/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx +++ b/frontend/src/Series/Index/Select/Tags/TagsModalContent.tsx @@ -16,6 +16,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props'; import Series from 'Series/Series'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import translate from 'Utilities/String/translate'; import styles from './TagsModalContent.css'; interface TagsModalContentProps { @@ -73,12 +74,12 @@ function TagsModalContent(props: TagsModalContentProps) { return ( - Tags + {translate('Tags')}
- Tags + {translate('Tags')} - Apply Tags + {translate('ApplyTags')} - Result + {translate('Result')}
{seriesTags.map((id) => { @@ -124,7 +125,11 @@ function TagsModalContent(props: TagsModalContentProps) { return (