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

Compare commits

..

14 Commits

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

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-31 22:26:09 -07:00
Bogdan
291d792810 Fixed: Moving files on import for usenet clients
Closes #7043
2024-08-01 01:17:10 -04:00
Mark McDowall
9b528eb829 New: Default file log level changed to debug 2024-08-01 01:16:24 -04:00
Mark McDowall
4c0b896174 Improve messaging for for Send Notifications setting in Emby / Jellyfin
Closes #7042
2024-07-31 22:16:01 -07:00
Bogdan
4ff83f9efc Fixed: Persist Indexer Flags for automatic imports
Revert "Fixed: Persist Indexer Flags when manual importing from queue"

This reverts commit 217611d716.
2024-08-01 01:15:36 -04:00
Bogdan
217611d716 Fixed: Persist Indexer Flags when manual importing from queue 2024-07-31 00:28:01 -04:00
Mark McDowall
1299a97579 Update React Lint rules for TSX 2024-07-30 21:27:33 -07:00
Mark McDowall
4c0de55672 Fixed: Setting page size in Queue, History and Blocklist
Closes #7035
2024-07-30 21:27:33 -07:00
Bogdan
78a0def46a Fixed: Moving files for torrents when Remove Completed is disabled 2024-07-31 00:27:19 -04:00
Mark McDowall
11a9dcb389 New: Return downloading magnets from Transmission
Closes #7029
2024-07-31 00:26:24 -04:00
Mark McDowall
4eab168267 New: Add metadata links to telegram messages
Closes #5342
---------

Co-authored-by: Ivar Stangeby <istangeby@gmail.com>
2024-07-31 00:25:48 -04:00
Bogdan
c9b5a1258a New: Title filter for Series Index 2024-07-30 21:25:10 -07:00
Mark McDowall
9127a91dfc Fixed: Allow leading/trailing spaces on non-Windows
Closes #6971
2024-07-30 21:25:00 -07:00
Weblate
cc85a28ff7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Wolfy The Broccoly <theproviderofsolace@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-30 21:24:50 -07:00
56 changed files with 386 additions and 173 deletions

View File

@@ -359,11 +359,16 @@ module.exports = {
],
rules: Object.assign(typescriptEslintRecommended.rules, {
'no-shadow': 'off',
// These should be enabled after cleaning things up
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'no-shadow': 'off',
'prettier/prettier': 'error',
'simple-import-sort/imports': [
'error',
@@ -376,7 +381,41 @@ module.exports = {
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
]
}
]
],
// React Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// React
'react/function-component-definition': 'error',
'react/hook-use-state': 'error',
'react/jsx-boolean-value': ['error', 'always'],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' }
],
'react/jsx-fragments': 'error',
'react/jsx-handler-names': [
'error',
{
eventHandlerPrefix: 'on',
eventHandlerPropPrefix: 'on'
}
],
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
'react/jsx-sort-props': [
'error',
{
callbacksLast: true,
noSortAlphabetically: true,
reservedFirst: true
}
],
'react/prop-types': 'off',
'react/self-closing-comp': 'error'
})
},
{

View File

@@ -59,6 +59,7 @@ function Blocklist() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isRemoving,
@@ -223,6 +224,7 @@ function Blocklist() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
@@ -264,6 +266,7 @@ function Blocklist() {
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}

View File

@@ -53,6 +53,7 @@ function History() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
} = useSelector((state: AppState) => state.history);
@@ -154,6 +155,7 @@ function History() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
@@ -193,6 +195,7 @@ function History() {
<div>
<Table
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}

View File

@@ -73,6 +73,7 @@ function Queue() {
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isGrabbing,
@@ -269,8 +270,10 @@ function Queue() {
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
@@ -344,6 +347,7 @@ function Queue() {
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
maxPageSize={200}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}

View File

@@ -1,11 +1,11 @@
import React, { Fragment, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { setQueueOption } from 'Store/Actions/queueActions';
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
@@ -22,24 +22,26 @@ function QueueOptions() {
[name]: value,
})
);
if (name === 'includeUnknownSeriesItems') {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
<FormGroup>
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownSeriesItems"
value={includeUnknownSeriesItems}
helpText={translate('ShowUnknownSeriesItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
</Fragment>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownSeriesItems"
value={includeUnknownSeriesItems}
helpText={translate('ShowUnknownSeriesItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
);
}

View File

@@ -35,6 +35,10 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
}
function AppRoutes() {
return (
<Switch>
@@ -51,9 +55,7 @@ function AppRoutes() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
addUrlBase={false}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
render={RedirectWithUrlBase}
/>
)}
@@ -61,21 +63,9 @@ function AppRoutes() {
<Route path="/add/import" component={ImportSeries} />
<Route
path="/serieseditor"
exact={true}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
/>
<Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} />
<Route
path="/seasonpass"
exact={true}
render={() => {
return <Redirect to={getPathWithUrlBase('/')} />;
}}
/>
<Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} />
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />

View File

@@ -64,7 +64,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
<div>{info.componentStack}</div>
)}
{<div className={styles.version}>Version: {window.Sonarr.version}</div>}
<div className={styles.version}>Version: {window.Sonarr.version}</div>
</details>
</div>
);

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
@@ -82,9 +82,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
<Popover
anchor={
<span>
{showSeasonNumber && seasonNumber != null && (
<Fragment>{seasonNumber}x</Fragment>
)}
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
@@ -111,9 +109,7 @@ function EpisodeNumber(props: EpisodeNumberProps) {
/>
) : (
<span>
{showSeasonNumber && seasonNumber != null && (
<Fragment>{seasonNumber}x</Fragment>
)}
{showSeasonNumber && seasonNumber != null && <>{seasonNumber}x</>}
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}

View File

@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const [isOpen, setIsOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
setIsOpen(true);
}, [setIsOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
setIsOpen(false);
}, [setIsOpen]);
return [isOpen, setModalOpen, setModalClosed];
}

View File

@@ -857,7 +857,7 @@ function InteractiveImportModalContent(
<MenuContent>
<SelectedMenuItem
name={'all'}
name="all"
isSelected={!filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@@ -865,7 +865,7 @@ function InteractiveImportModalContent(
</SelectedMenuItem>
<SelectedMenuItem
name={'new'}
name="new"
isSelected={filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@@ -945,7 +945,7 @@ function InteractiveImportModalContent(
<SelectInput
className={styles.bulkSelect}
name="select"
value={'select'}
value="select"
values={bulkSelectOptions}
isDisabled={!selectedIds.length}
onChange={onSelectModalSelect}

View File

@@ -17,7 +17,7 @@ function SelectLanguageModal(props: SelectLanguageModalProps) {
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<SelectLanguageModalContent
languageIds={languageIds}
modalTitle={modalTitle}

View File

@@ -64,19 +64,20 @@ interface RowItemData {
onSeriesSelect(seriesId: number): void;
}
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, columns, onSeriesSelect } = data;
const series = index >= items.length ? null : items[index];
if (index >= items.length) {
const handlePress = useCallback(() => {
if (series?.id) {
onSeriesSelect(series.id);
}
}, [series?.id, onSeriesSelect]);
if (series == null) {
return null;
}
const series = items[index];
return (
<VirtualTableRowButton
style={{
@@ -84,7 +85,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
justifyContent: 'space-between',
...style,
}}
onPress={() => onSeriesSelect(series.id)}
onPress={handlePress}
>
<SelectSeriesRow
key={series.id}
@@ -98,7 +99,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
/>
</VirtualTableRowButton>
);
};
}
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
const { modalTitle, onSeriesSelect, onModalClose } = props;
@@ -197,9 +198,9 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
/>
<Scroller
ref={scrollerRef}
className={styles.scroller}
autoFocus={false}
ref={scrollerRef}
>
<SelectSeriesModalTableHeader columns={columns} />
<List<RowItemData>

View File

@@ -17,7 +17,7 @@ function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<SelectDownloadClientModalContent
protocol={protocol}
modalTitle={modalTitle}

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import ParseModal from 'Parse/ParseModal';
@@ -16,7 +16,7 @@ function ParseToolbarButton() {
}, [setIsParseModalOpen]);
return (
<Fragment>
<>
<PageToolbarButton
label={translate('TestParsing')}
iconName={icons.PARSE}
@@ -24,7 +24,7 @@ function ParseToolbarButton() {
/>
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
</Fragment>
</>
);
}

View File

@@ -42,11 +42,7 @@ interface SeriesIndexOverviewsProps {
isSmallScreen: boolean;
}
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, ...otherData } = data;
if (index >= items.length) {
@@ -60,7 +56,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
<SeriesIndexOverview seriesId={series.id} {...otherData} />
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@@ -60,12 +60,12 @@ const seriesIndexSelector = createSelector(
}
);
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
function Cell({
columnIndex,
rowIndex,
style,
data,
}) => {
}: GridChildComponentProps<CellItemData>) {
const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, posterWidth, posterHeight } = layout;
const index = rowIndex * columnCount + columnIndex;
@@ -92,7 +92,7 @@ const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
/>
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@@ -45,11 +45,7 @@ const columnsSelector = createSelector(
(columns) => columns
);
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, sortKey, columns, isSelectMode } = data;
if (index >= items.length) {
@@ -75,7 +71,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
/>
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -32,7 +32,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
);
return (
<Fragment>
<>
<FormGroup>
<FormLabel>{translate('ShowBanners')}</FormLabel>
@@ -56,7 +56,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
</Fragment>
</>
);
}

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
@@ -17,11 +17,11 @@ function CustomFormatSettingsPage() {
// @ts-ignore
showSave={false}
additionalButtons={
<Fragment>
<>
<PageToolbarSeparator />
<ParseToolbarButton />
</Fragment>
</>
}
/>

View File

@@ -231,9 +231,9 @@ function ManageDownloadClientsModalContent(
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
<ManageDownloadClientsEditModal
isOpen={isEditModalOpen}
downloadClientIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
downloadClientIds={selectedIds}
/>
<TagsModal

View File

@@ -261,9 +261,9 @@ function ManageImportListsModalContent(
<ManageImportListsEditModal
isOpen={isEditModalOpen}
importListIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
importListIds={selectedIds}
/>
<TagsModal

View File

@@ -226,9 +226,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
@@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
<ManageIndexersEditModal
isOpen={isEditModalOpen}
indexerIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
indexerIds={selectedIds}
/>
<TagsModal

View File

@@ -251,6 +251,11 @@ export const filterBuilderProps = [
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.SERIES_TYPES
},
{
name: 'title',
label: () => translate('Title'),
type: filterBuilderTypes.STRING
},
{
name: 'network',
label: () => translate('Network'),

View File

@@ -66,7 +66,7 @@ function About() {
) : null}
{isDocker ? (
<DescriptionListItem title={translate('Docker')} data={'Yes'} />
<DescriptionListItem title={translate('Docker')} data="Yes" />
) : null}
<DescriptionListItem

View File

@@ -1,10 +1,4 @@
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
@@ -158,7 +152,7 @@ function Updates() {
{translate('InstallLatest')}
</SpinnerButton>
) : (
<Fragment>
<>
<Icon name={icons.WARNING} kind={kinds.WARNING} size={30} />
<div className={styles.message}>
@@ -171,7 +165,7 @@ function Updates() {
}
/>
</div>
</Fragment>
</>
)}
{isFetching ? (

View File

@@ -380,8 +380,17 @@ namespace NzbDrone.Common.Test
[TestCase(@" C:\Test\TV\")]
[TestCase(@" C:\Test\TV")]
public void IsPathValid_should_be_false(string path)
public void IsPathValid_should_be_false_on_windows(string path)
{
WindowsOnly();
path.IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
}
[TestCase(@"")]
[TestCase(@"relative/path")]
public void IsPathValid_should_be_false_on_unix(string path)
{
PosixOnly();
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
}
}

View File

@@ -152,16 +152,20 @@ namespace NzbDrone.Common.Extensions
return false;
}
var directoryInfo = new DirectoryInfo(path);
while (directoryInfo != null)
// Only check for leading or trailing spaces for path when running on Windows.
if (OsInfo.IsWindows)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
var directoryInfo = new DirectoryInfo(path);
directoryInfo = directoryInfo.Parent;
while (directoryInfo != null)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
directoryInfo = directoryInfo.Parent;
}
}
if (validationType == PathValidationType.AnyOs)

View File

@@ -49,10 +49,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
[Test]
public void magnet_download_should_not_return_the_item()
public void magnet_download_should_be_returned_as_queued()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
@@ -60,7 +60,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests
public void magnet_download_should_not_return_the_item()
{
PrepareClientToReturnMagnetItem();
Subject.GetItems().Count().Should().Be(0);
var item = Subject.GetItems().Single();
item.Status.Should().Be(DownloadItemStatus.Queued);
}
[Test]

View File

@@ -74,8 +74,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
.Returns(new List<EpisodeHistory>());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew()
.With(d => d.OutputPath = new OsPath(outputPath))
.Build();
.With(d => d.OutputPath = new OsPath(outputPath))
.Build();
}
private void GivenNewDownload()

View File

@@ -220,7 +220,7 @@ namespace NzbDrone.Core.Configuration
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "debug").ToLowerInvariant();
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);

View File

@@ -129,10 +129,8 @@ namespace NzbDrone.Core.Download.Clients.Aria2
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(GetOutputPath(torrent)));
yield return new DownloadClientItem
var queueItem = new DownloadClientItem
{
CanMoveFiles = false,
CanBeRemoved = torrent.Status == "complete",
Category = null,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.InfoHash?.ToUpper(),
@@ -146,7 +144,12 @@ namespace NzbDrone.Core.Download.Clients.Aria2
Status = status,
Title = title,
TotalSize = totalLength,
CanMoveFiles = false
};
queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == "complete";
yield return queueItem;
}
}

View File

@@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))
{
yield return new DownloadClientItem
var queueItem = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = Definition.Name + "_" + item.DownloadId,
@@ -101,11 +101,14 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
OutputPath = item.OutputPath,
Status = item.Status,
CanMoveFiles = !Settings.ReadOnly,
CanBeRemoved = !Settings.ReadOnly
Status = item.Status
};
queueItem.CanMoveFiles = queueItem.CanBeRemoved =
queueItem.DownloadClientInfo.RemoveCompletedDownloads &&
!Settings.ReadOnly;
yield return queueItem;
}
}

View File

@@ -190,6 +190,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
// Here we detect if Deluge is managing the torrent and whether the seed criteria has been met.
// This allows Sonarr to delete the torrent as appropriate.
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
torrent.IsAutoManaged &&
torrent.StopAtRatio &&
torrent.Ratio >= torrent.StopRatio &&

View File

@@ -88,7 +88,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
}
}
var item = new DownloadClientItem()
var item = new DownloadClientItem
{
Category = Settings.TvCategory,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
@@ -99,11 +99,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
RemainingTime = GetRemainingTime(torrent),
SeedRatio = GetSeedRatio(torrent),
Status = GetStatus(torrent),
Message = GetMessage(torrent),
CanMoveFiles = IsFinished(torrent),
CanBeRemoved = IsFinished(torrent)
Message = GetMessage(torrent)
};
item.CanMoveFiles = item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && IsFinished(torrent);
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
{
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);

View File

@@ -153,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
item.Status = DownloadItemStatus.Downloading;
}
if (item.Status == DownloadItemStatus.Completed)
if (item.DownloadClientInfo.RemoveCompletedDownloads && item.Status == DownloadItemStatus.Completed)
{
// Grab cached seedConfig
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(item.DownloadId);
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
// Check if seed ratio reached
item.CanMoveFiles = item.CanBeRemoved = true;
}
else if (properties.DateFinished != null && properties.DateFinished > 0)
else if (properties.DateFinished is > 0)
{
// Check if seed time reached
if ((DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds((long)properties.DateFinished)) >= seedConfig.SeedTime)

View File

@@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
break;
}
item.CanBeRemoved = item.CanMoveFiles = torrent.Status == FreeboxDownloadTaskStatus.Done;
item.CanBeRemoved = item.CanMoveFiles = item.DownloadClientInfo.RemoveCompletedDownloads && torrent.Status == FreeboxDownloadTaskStatus.Done;
queueItems.Add(item);
}

View File

@@ -92,7 +92,10 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
item.Status = DownloadItemStatus.Downloading;
}
item.CanMoveFiles = item.CanBeRemoved = torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused;
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
torrent.IsFinished &&
torrent.State == HadoukenTorrentState.Paused;
items.Add(item);
}

View File

@@ -225,7 +225,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
foreach (var torrent in torrents)
{
var item = new DownloadClientItem()
var item = new DownloadClientItem
{
DownloadId = torrent.Hash.ToUpper(),
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
@@ -239,7 +239,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
torrent.State is "pausedUP" or "stoppedUP" &&
HasReachedSeedLimit(torrent, config);
switch (torrent.State)
{

View File

@@ -43,12 +43,6 @@ namespace NzbDrone.Core.Download.Clients.Transmission
foreach (var torrent in torrents)
{
// If totalsize == 0 the torrent is a magnet downloading metadata
if (torrent.TotalSize == 0)
{
continue;
}
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
@@ -99,6 +93,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Status = DownloadItemStatus.Warning;
item.Message = torrent.ErrorString;
}
else if (torrent.TotalSize == 0)
{
item.Status = DownloadItemStatus.Queued;
}
else if (torrent.LeftUntilDone == 0 && (torrent.Status == TransmissionTorrentStatus.Stopped ||
torrent.Status == TransmissionTorrentStatus.Seeding ||
torrent.Status == TransmissionTorrentStatus.SeedingWait))
@@ -119,7 +117,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Status = DownloadItemStatus.Downloading;
}
item.CanBeRemoved = HasReachedSeedLimit(torrent, item.SeedRatio, configFunc);
item.CanBeRemoved = item.DownloadClientInfo.RemoveCompletedDownloads && HasReachedSeedLimit(torrent, item.SeedRatio, configFunc);
item.CanMoveFiles = item.CanBeRemoved && torrent.Status == TransmissionTorrentStatus.Stopped;
items.Add(item);

View File

@@ -185,7 +185,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
// Grab cached seedConfig
var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash);
if (torrent.IsFinished && seedConfig != null)
if (item.DownloadClientInfo.RemoveCompletedDownloads && torrent.IsFinished && seedConfig != null)
{
var canRemove = false;

View File

@@ -167,6 +167,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
// 'Started' without 'Queued' is when the torrent is 'forced seeding'
item.CanMoveFiles = item.CanBeRemoved =
item.DownloadClientInfo.RemoveCompletedDownloads &&
!torrent.Status.HasFlag(UTorrentTorrentStatus.Queued) &&
!torrent.Status.HasFlag(UTorrentTorrentStatus.Started);

View File

@@ -37,6 +37,7 @@ namespace NzbDrone.Core.Download
public string Type { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool HasPostImportCategory { get; set; }
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
@@ -49,6 +50,7 @@ namespace NzbDrone.Core.Download
Type = downloadClient.Name,
Id = downloadClient.Definition.Id,
Name = downloadClient.Definition.Name,
RemoveCompletedDownloads = downloadClient.Definition is DownloadClientDefinition { RemoveCompletedDownloads: true },
HasPostImportCategory = hasPostImportCategory
};
}

View File

@@ -1,4 +1,6 @@
{
"AddAutoTag": "أضف كلمات دلالية تلقائيا",
"AddCondition": "إضافة شرط"
"AddCondition": "إضافة شرط",
"AutoTaggingNegateHelpText": "إذا تم تحديده ، فلن يتم تطبيق التنسيق المخصص إذا تطابق شرط {implementationName} هذا.",
"ConnectionLostReconnect": "سيحاول {appName} الاتصال تلقائيًا ، أو يمكنك النقر فوق إعادة التحميل أدناه."
}

View File

@@ -1325,8 +1325,8 @@
"NotificationsEmailSettingsUseEncryption": "Use Encryption",
"NotificationsEmailSettingsUseEncryptionHelpText": "Whether to prefer using encryption if configured on the server, to always use encryption via SSL (Port 465 only) or StartTLS (any other port) or to never use encryption",
"NotificationsEmbySettingsSendNotifications": "Send Notifications",
"NotificationsEmbySettingsSendNotificationsHelpText": "Have MediaBrowser send notifications to configured providers",
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete?",
"NotificationsEmbySettingsSendNotificationsHelpText": "Have Emby send notifications to configured providers. Not supported on Jellyfin.",
"NotificationsEmbySettingsUpdateLibraryHelpText": "Update Library on Import, Rename, or Delete",
"NotificationsGotifySettingIncludeSeriesPoster": "Include Series Poster",
"NotificationsGotifySettingIncludeSeriesPosterHelpText": "Include series poster in message",
"NotificationsGotifySettingsAppToken": "App Token",
@@ -1429,6 +1429,8 @@
"NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages",
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
"NotificationsTelegramSettingsMetadataLinks": "Metadata Links",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Add a links to series metadata when sending notifications",
"NotificationsTelegramSettingsSendSilently": "Send Silently",
"NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound",
"NotificationsTelegramSettingsTopicId": "Topic ID",

View File

@@ -1839,7 +1839,7 @@
"Titles": "Títulos",
"ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar",
"TotalFileSize": "Tamaño total de archivo",
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización",
"UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización: {version}",
"UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado",
"UrlBase": "URL base",
"UseSsl": "Usar SSL",
@@ -2093,5 +2093,12 @@
"CountVotes": "{votes} votos",
"InstallMajorVersionUpdateMessage": "Esta actualización instalará una nueva versión principal y podría no ser compatible con tu sistema. ¿Estás seguro que quieres instalar esta actualización?",
"InstallMajorVersionUpdateMessageLink": "Por favor revisa [{domain}]({url}) para más información.",
"NextAiringDate": "Siguiente emisión: {date}"
"NextAiringDate": "Siguiente emisión: {date}",
"SeasonsMonitoredAll": "Todas",
"SeasonsMonitoredNone": "Ninguna",
"SeasonsMonitoredStatus": "Temporadas monitorizadas",
"NoBlocklistItems": "Ningún elemento en la lista de bloqueo",
"SeasonsMonitoredPartial": "Parcial",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Añade un enlace a los metadatos de la serie cuando se envían notificaciones",
"NotificationsTelegramSettingsMetadataLinks": "Enlaces de metadatos"
}

View File

@@ -452,7 +452,7 @@
"RootFolderSelectFreeSpace": "{freeSpace} Libre",
"WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)",
"RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?",
"UpdateAll": "Tout actualiser",
"UpdateAll": "Actualiser",
"EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif",
"UnmonitorDeletedEpisodesHelpText": "Les épisodes effacés du disque dur ne seront plus surveillés dans {appName}",
"RssSync": "Synchronisation RSS",
@@ -2076,5 +2076,7 @@
"UnableToImportAutomatically": "Impossible d'importer automatiquement",
"DayOfWeekAt": "{day} à {time}",
"TomorrowAt": "Demain à {time}",
"TodayAt": "Aujourd'hui à {time}"
"TodayAt": "Aujourd'hui à {time}",
"ShowTagsHelpText": "Afficher les labels sous l'affiche",
"ShowTags": "Afficher les labels"
}

View File

@@ -51,7 +51,7 @@
"SizeOnDisk": "Tamanho no disco",
"SystemTimeHealthCheckMessage": "A hora do sistema está desligada por mais de 1 dia. Tarefas agendadas podem não ser executadas corretamente até que o horário seja corrigido",
"Unmonitored": "Não monitorado",
"UpdateAvailableHealthCheckMessage": "Nova atualização está disponível",
"UpdateAvailableHealthCheckMessage": "Nova atualização disponível: {version}",
"Added": "Adicionado",
"ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração",
"RemoveCompletedDownloads": "Remover downloads concluídos",
@@ -2093,5 +2093,12 @@
"Install": "Instalar",
"InstallMajorVersionUpdate": "Instalar Atualização",
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.",
"NextAiringDate": "Próxima Exibição: {date}"
"NextAiringDate": "Próxima Exibição: {date}",
"SeasonsMonitoredAll": "Todas",
"SeasonsMonitoredPartial": "Parcial",
"SeasonsMonitoredNone": "Nenhuma",
"SeasonsMonitoredStatus": "Temporadas monitoradas",
"NoBlocklistItems": "Sem itens na lista de bloqueio",
"NotificationsTelegramSettingsMetadataLinks": "Links de Metadados",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Adicione links aos metadados da série ao enviar notificações"
}

View File

@@ -140,7 +140,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
default:
case ImportMode.Auto:
copyOnly = downloadClientItem != null && !downloadClientItem.CanMoveFiles;
copyOnly = downloadClientItem is { CanMoveFiles: false };
break;
case ImportMode.Move:
copyOnly = false;

View File

@@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
@@ -29,6 +30,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
private readonly IAggregationService _aggregationService;
private readonly IDiskProvider _diskProvider;
private readonly IDetectSample _detectSample;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly ICustomFormatCalculationService _formatCalculator;
private readonly Logger _logger;
@@ -37,6 +39,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
IAggregationService aggregationService,
IDiskProvider diskProvider,
IDetectSample detectSample,
ITrackedDownloadService trackedDownloadService,
ICustomFormatCalculationService formatCalculator,
Logger logger)
{
@@ -45,6 +48,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_aggregationService = aggregationService;
_diskProvider = diskProvider;
_detectSample = detectSample;
_trackedDownloadService = trackedDownloadService;
_formatCalculator = formatCalculator;
_logger = logger;
}
@@ -145,6 +149,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}
else
{
if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true)
{
var trackedDownload = _trackedDownloadService.Find(downloadClientItem.DownloadId);
if (trackedDownload?.RemoteEpisode?.Release?.IndexerFlags != null)
{
localEpisode.IndexerFlags = trackedDownload.RemoteEpisode.Release.IndexerFlags;
}
}
localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode);
localEpisode.CustomFormatScore = localEpisode.Series.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -24,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby
var path = "/Notifications/Admin";
var request = BuildRequest(path, settings);
request.Headers.ContentType = "application/json";
request.LogHttpError = false;
request.SetContent(new
{
@@ -32,7 +34,21 @@ namespace NzbDrone.Core.Notifications.Emby
ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png"
}.ToJson());
ProcessRequest(request, settings);
try
{
ProcessRequest(request, settings);
}
catch (HttpException e)
{
if (e.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Warn("Unable to send notification to Emby. If you're using Jellyfin disable 'Send Notifications'");
}
else
{
throw;
}
}
}
public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series)

View File

@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Notifications.Emby
public MediaBrowserSettings()
{
Port = 8096;
UpdateLibrary = true;
}
[FieldDefinition(0, Label = "Host")]

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Notifications.Telegram
{
@@ -19,71 +20,77 @@ namespace NzbDrone.Core.Notifications.Telegram
public override void OnGrab(GrabMessage grabMessage)
{
var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE;
var links = GetLinks(grabMessage.Series);
_proxy.SendNotification(title, grabMessage.Message, Settings);
_proxy.SendNotification(title, grabMessage.Message, links, Settings);
}
public override void OnDownload(DownloadMessage message)
{
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
var links = GetLinks(message.Series);
_proxy.SendNotification(title, message.Message, Settings);
_proxy.SendNotification(title, message.Message, links, Settings);
}
public override void OnImportComplete(ImportCompleteMessage message)
{
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
var links = GetLinks(message.Series);
_proxy.SendNotification(title, message.Message, Settings);
_proxy.SendNotification(title, message.Message, links, Settings);
}
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
{
var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE;
var links = GetLinks(deleteMessage.Series);
_proxy.SendNotification(title, deleteMessage.Message, Settings);
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
}
public override void OnSeriesAdd(SeriesAddMessage message)
{
var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE;
var links = GetLinks(message.Series);
_proxy.SendNotification(title, message.Message, Settings);
_proxy.SendNotification(title, message.Message, links, Settings);
}
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
{
var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE;
var links = GetLinks(deleteMessage.Series);
_proxy.SendNotification(title, deleteMessage.Message, Settings);
_proxy.SendNotification(title, deleteMessage.Message, links, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE;
_proxy.SendNotification(title, healthCheck.Message, Settings);
_proxy.SendNotification(title, healthCheck.Message, null, Settings);
}
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
{
var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE;
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings);
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", null, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE;
_proxy.SendNotification(title, updateMessage.Message, Settings);
_proxy.SendNotification(title, updateMessage.Message, null, Settings);
}
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
{
var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE;
_proxy.SendNotification(title, message.Message, Settings);
_proxy.SendNotification(title, message.Message, null, Settings);
}
public override ValidationResult Test()
@@ -94,5 +101,37 @@ namespace NzbDrone.Core.Notifications.Telegram
return new ValidationResult(failures);
}
private List<TelegramLink> GetLinks(Series series)
{
var links = new List<TelegramLink>();
foreach (var link in Settings.MetadataLinks)
{
var linkType = (MetadataLinkType)link;
if (linkType == MetadataLinkType.Imdb && series.ImdbId.IsNotNullOrWhiteSpace())
{
links.Add(new TelegramLink("IMDb", $"https://www.imdb.com/title/{series.ImdbId}"));
}
if (linkType == MetadataLinkType.Tvdb && series.TvdbId > 0)
{
links.Add(new TelegramLink("TVDb", $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}"));
}
if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0)
{
links.Add(new TelegramLink("TVMaze", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show"));
}
if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0)
{
links.Add(new TelegramLink("Trakt", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_"));
}
}
return links;
}
}
}

View File

@@ -0,0 +1,14 @@
namespace NzbDrone.Core.Notifications.Telegram
{
public class TelegramLink
{
public string Label { get; set; }
public string Link { get; set; }
public TelegramLink(string label, string link)
{
Label = label;
Link = link;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Web;
using FluentValidation.Results;
using NLog;
@@ -13,7 +14,7 @@ namespace NzbDrone.Core.Notifications.Telegram
{
public interface ITelegramProxy
{
void SendNotification(string title, string message, TelegramSettings settings);
void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings);
ValidationFailure Test(TelegramSettings settings);
}
@@ -32,10 +33,16 @@ namespace NzbDrone.Core.Notifications.Telegram
_logger = logger;
}
public void SendNotification(string title, string message, TelegramSettings settings)
public void SendNotification(string title, string message, List<TelegramLink> links, TelegramSettings settings)
{
// Format text to add the title before and bold using markdown
var text = $"<b>{HttpUtility.HtmlEncode(title)}</b>\n{HttpUtility.HtmlEncode(message)}";
var text = new StringBuilder($"<b>{HttpUtility.HtmlEncode(title)}</b>\n");
text.AppendLine(HttpUtility.HtmlEncode(message));
foreach (var link in links)
{
text.AppendLine($"<a href=\"{link.Link}\">{HttpUtility.HtmlEncode(link.Label)}</a>");
}
var requestBuilder = new HttpRequestBuilder(URL).Resource("bot{token}/sendmessage").Post();
@@ -58,7 +65,12 @@ namespace NzbDrone.Core.Notifications.Telegram
const string title = "Test Notification";
const string body = "This is a test message from Sonarr";
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings);
var links = new List<TelegramLink>
{
new TelegramLink("Sonarr.tv", "https://sonarr.tv")
};
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, links, settings);
}
catch (Exception ex)
{

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Telegram
{
public class TelegramSettingsValidator : AbstractValidator<TelegramSettings>
@@ -12,6 +14,16 @@ namespace NzbDrone.Core.Notifications.Telegram
RuleFor(c => c.ChatId).NotEmpty();
RuleFor(c => c.TopicId).Must(topicId => !topicId.HasValue || topicId > 1)
.WithMessage("Topic ID must be greater than 1 or empty");
RuleFor(c => c.MetadataLinks).Custom((links, context) =>
{
foreach (var link in links)
{
if (!Enum.IsDefined(typeof(MetadataLinkType), link))
{
context.AddFailure("MetadataLinks", $"MetadataLink is not valid: {link}");
}
}
});
}
}
@@ -19,6 +31,11 @@ namespace NzbDrone.Core.Notifications.Telegram
{
private static readonly TelegramSettingsValidator Validator = new ();
public TelegramSettings()
{
MetadataLinks = Enumerable.Empty<int>();
}
[FieldDefinition(0, Label = "NotificationsTelegramSettingsBotToken", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://core.telegram.org/bots")]
public string BotToken { get; set; }
@@ -34,9 +51,27 @@ namespace NzbDrone.Core.Notifications.Telegram
[FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")]
public bool IncludeAppNameInTitle { get; set; }
[FieldDefinition(5, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")]
public IEnumerable<int> MetadataLinks { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public enum MetadataLinkType
{
[FieldOption(Label = "IMDb")]
Imdb,
[FieldOption(Label = "TVDb")]
Tvdb,
[FieldOption(Label = "TVMaze")]
Tvmaze,
[FieldOption(Label = "Trakt")]
Trakt,
}
}