Compare commits

..

No commits in common. "develop" and "v6.0.1.10258" have entirely different histories.

36 changed files with 137 additions and 556 deletions

View file

@ -1,186 +1,13 @@
# How to Contribute
We're always looking for people to help make Radarr even better, there are a number of ways to contribute.
# Documentation
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/radarr/contributing).
Setup guides, [FAQ](/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better.
## Documentation
# Development
Setup guides, [FAQ](https://wiki.servarr.com/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better.
Radarr is written in C# (backend) and JS (frontend). The backend is built on the .NET6 (and _soon_ .NET8) framework, while the frontend utilizes Reactjs.
## Development
## Tools required
- Visual Studio 2022 or higher is recommended (<https://www.visualstudio.com/vs/>). The community version is free and works (<https://www.visualstudio.com/downloads/>).
> VS 2022 V17.0 or higher is recommended as it includes the .NET6 SDK
{.is-info}
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- The [Node.js](https://nodejs.org/) runtime is required. The following versions are supported:
- **20** (any minor or patch version within this)
{.grid-list}
> The Application will **NOT** run on older versions such as `18.x`, `16.x` or any version below 20.0! Due to a dependency issue, it will also not run on `21.x` and is untested on other verisons.
{.is-warning}
- [Yarn](https://yarnpkg.com/getting-started/install) is required to build the frontend
- Yarn is included with **Node 20**+ by default. Enable it with `corepack enable`
- For other Node versions, install it with `npm i -g corepack`
## Getting started
1. Fork Radarr
1. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
> Be sure to run lint `yarn lint --fix` on your code for any front end changes before committing.
For css changes `yarn stylelint-windows --fix` {.is-info}
### Building the frontend
- Navigate to the cloned directory
- Install the required Node Packages
```bash
yarn install
```
- Start webpack to monitor your development environment for any changes that need post processing using:
```bash
yarn start
```
### Building the Backend
The backend solution is most easily built and ran in Visual Studio or Rider, however if the only priority is working on the frontend UI it can be built easily from command line as well when the correct SDK is installed.
#### Visual Studio
> Ensure startup project is set to `Radarr.Console` and framework to `net6.0`
{.is-info}
1. First `Build` the solution in Visual Studio, this will ensure all projects are correctly built and dependencies restored
1. Next `Debug/Run` the project in Visual Studio to start Radarr
1. Open <http://localhost:7878>
#### Command line
1. Clean solution
```shell
dotnet clean src/Radarr.sln -c Debug
```
1. Restore and Build debug configuration for the correct platform (Posix or Windows)
```shell
dotnet msbuild -restore src/Radarr.sln -p:Configuration=Debug -p:Platform=Posix -t:PublishAllRids
```
1. Run the produced executable from `/_output`
## Contributing Code
- If you're adding a new, already requested feature, please comment on [GitHub Issues](https://github.com/Radarr/Radarr/issues) so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Radarr's develop branch, do not merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the discord if you have any questions
- Add tests (unit/integration)
- Commit with \*nix line endings for consistency (We checkout Windows and commit \*nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2022 and WebStorm
## Pull Requesting
- Only make pull requests to `develop`, never `master`, if you make a PR to `master` we will comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
- `new-feature` (Good)
- `fix-bug` (Good)
- `patch` (Bad)
- `develop` (Bad)
- Commits should be wrote as `New:` or `Fixed:` for changes that would not be considered a `maintenance release`
## Unit Testing
Radarr utilizes nunit for its unit, integration, and automation test suite.
### Running Tests
Tests can be run easily from within VS using the included nunit3testadapter nuget package or from the command line using the included bash script `test.sh`.
From VS simply navigate to Test Explorer and run or debug the tests you'd like to examine.
Tests can be run all at once or one at a time in VS.
From command line the `test.sh` script accepts 3 parameters
```bash
test.sh <PLATFORM> <TYPE> <COVERAGE>
```
### Writing Tests
While not always fun, we encourage writing unit tests for any backend code changes. This will ensure the change is functioning as you intended and that future changes dont break the expected behavior.
> We currently require 80% coverage on new code when submitting a PR
{.is-info}
If you have any questions about any of this, please let us know.
# Translation
Radarr uses a self hosted open access [Weblate](https://translate.servarr.com) instance to manage its json translation files. These files are stored in the repo at `src/NzbDrone.Core/Localization`
## Contributing to an Existing Translation
Weblate handles synchronization and translation of strings for all languages other than English. Editing of translated strings and translating existing strings for supported languages should be performed there for the Radarr project.
The English translation, `en.json`, serves as the source for all other translations and is managed on GitHub repo.
## Adding a Language
Adding translations to Radarr requires two steps
- Adding the Language to weblate
- Adding the Language to Radarr codebase
## Adding Translation Strings in Code
The English translation, `src/NzbDrone.Core/Localization/en.json`, serves as the source for all other translations and is managed on GitHub repo. When adding a new string to either the UI or backend a key must also be added to `en.json` along with the default value in English. This key may then be consumed as follows:
> PRs for translation of log messages will not be accepted
{.is-warning}
### Backend Strings
Backend strings may be added utilizing the Localization Service `GetLocalizedString` method
```dotnet
private readonly ILocalizationService _localizationService;
public IndexerCheck(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
var translated = _localizationService.GetLocalizedString("IndexerHealthCheckNoIndexers")
```
### Frontend Strings
New strings can be added to the frontend by importing the translate function and using a key specified from `en.json`
```js
import translate from 'Utilities/String/translate';
<div>
{translate('UnableToAddANewIndexerPleaseTryAgain')}
</div>
```
See the [Wiki Page](https://wiki.servarr.com/radarr/contributing)

View file

@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '6.1.0'
majorVersion: '6.0.1'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@ -18,9 +18,9 @@ variables:
dotnetVersion: '8.0.405'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2025'
linuxImage: 'ubuntu-24.04'
macImage: 'macOS-15'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-22.04'
macImage: 'macOS-13'
trigger:
branches:

View file

@ -19,7 +19,6 @@
.modal {
position: relative;
display: flex;
max-width: 90%;
max-height: 90%;
border-radius: 6px;
opacity: 1;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,56 +0,0 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import themes from 'Styles/Themes';
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
(theme) => theme
);
}
const useTheme = () => {
const selectedTheme = useSelector(createThemeSelector());
const [resolvedTheme, setResolvedTheme] = useState(selectedTheme);
useEffect(() => {
if (selectedTheme !== 'auto') {
setResolvedTheme(selectedTheme);
return;
}
const applySystemTheme = () => {
setResolvedTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
);
};
applySystemTheme();
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', applySystemTheme);
return () => {
window
.matchMedia('(prefers-color-scheme: dark)')
.removeEventListener('change', applySystemTheme);
};
}, [selectedTheme]);
return resolvedTheme;
};
export default useTheme;
export const useThemeColor = (color: string) => {
const theme = useTheme();
const themeVariables = themes[theme];
// @ts-expect-error - themeVariables is a string indexable type
return themeVariables[color];
};

View file

@ -67,7 +67,6 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
monitored,
status,
path,
titleSlug,
overview,
statistics = {} as Statistics,
images,
@ -142,9 +141,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
<div className={styles.content}>
<div className={styles.poster}>
<div className={styles.posterContainer}>
{isSelectMode ? (
<MovieIndexPosterSelect movieId={movieId} titleSlug={titleSlug} />
) : null}
{isSelectMode ? <MovieIndexPosterSelect movieId={movieId} /> : null}
{status === 'deleted' ? (
<div className={styles.deleted} title={translate('Deleted')} />

View file

@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import React, { SyntheticEvent, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
@ -69,7 +70,6 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
monitored,
status,
images,
titleSlug,
tmdbId,
imdbId,
youTubeTrailerId,
@ -142,7 +142,30 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
setIsDeleteMovieModalOpen(false);
}, [setIsDeleteMovieModalOpen]);
const link = `/movie/${titleSlug}`;
const [selectState, selectDispatch] = useSelect();
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`/movie/${tmdbId}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
id: movieId,
isSelected: !selectState.selectedState[movieId],
shiftKey,
});
},
[movieId, selectState.selectedState, selectDispatch, tmdbId]
);
const link = `/movie/${tmdbId}`;
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = {
width: `${posterWidth}px`,
@ -152,9 +175,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
return (
<div className={styles.content}>
<div className={styles.posterContainer} title={title}>
{isSelectMode ? (
<MovieIndexPosterSelect movieId={movieId} titleSlug={titleSlug} />
) : null}
{isSelectMode ? <MovieIndexPosterSelect movieId={movieId} /> : null}
<Label className={styles.controls}>
<SpinnerIconButton
@ -199,7 +220,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
<div className={styles.deleted} title={translate('Deleted')} />
) : null}
<Link className={styles.link} style={elementStyle} to={link}>
<Link className={styles.link} style={elementStyle} {...linkProps}>
<MoviePoster
style={elementStyle}
images={images}

View file

@ -3,8 +3,8 @@
top: 0;
left: 0;
z-index: 3;
width: 100%;
height: 100%;
width: 36px;
height: 36px;
}
.checkContainer {

View file

@ -7,23 +7,15 @@ import styles from './MovieIndexPosterSelect.css';
interface MovieIndexPosterSelectProps {
movieId: number;
titleSlug: string;
}
function MovieIndexPosterSelect({
movieId,
titleSlug,
}: MovieIndexPosterSelectProps) {
function MovieIndexPosterSelect(props: MovieIndexPosterSelectProps) {
const { movieId } = props;
const [selectState, selectDispatch] = useSelect();
const isSelected = selectState.selectedState[movieId];
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, PointerEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`${window.Radarr.urlBase}/movie/${titleSlug}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
@ -33,7 +25,7 @@ function MovieIndexPosterSelect({
shiftKey,
});
},
[movieId, titleSlug, isSelected, selectDispatch]
[movieId, isSelected, selectDispatch]
);
return (

View file

@ -23,6 +23,14 @@ function Donations() {
/>
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://readarr.com/donate">
<img
className={styles.logo}
src={`${window.Radarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
/>
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://prowlarr.com/donate">
<img

View file

@ -18,19 +18,9 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import Movie from 'Movie/Movie';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearMovieFiles,
fetchMovieFiles,
} from 'Store/Actions/movieFileActions';
import {
clearQueueDetails,
fetchQueueDetails,
} from 'Store/Actions/queueActions';
import {
batchToggleCutoffUnmetMovies,
clearCutoffUnmet,
@ -45,8 +35,6 @@ import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import {
registerPagePopulator,
unregisterPagePopulator,
@ -120,8 +108,6 @@ function CutoffUnmet() {
const isSearchingForMovies =
isSearchingForAllMovies || isSearchingForSelectedMovies;
const previousItems = usePrevious(items);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
@ -218,8 +204,6 @@ function CutoffUnmet() {
return () => {
dispatch(clearCutoffUnmet());
dispatch(clearQueueDetails());
dispatch(clearMovieFiles());
};
}, [requestCurrentPage, dispatch]);
@ -239,21 +223,6 @@ function CutoffUnmet() {
};
}, [dispatch]);
useEffect(() => {
if (!previousItems || hasDifferentItems(items, previousItems)) {
const movieIds = selectUniqueIds<Movie, number>(items, 'id');
const movieFileIds = selectUniqueIds<Movie, number>(items, 'movieFileId');
if (movieIds.length) {
dispatch(fetchQueueDetails({ movieIds }));
}
if (movieFileIds.length) {
dispatch(fetchMovieFiles({ movieFileIds }));
}
}
}, [items, previousItems, dispatch]);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>

View file

@ -18,16 +18,10 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import Movie from 'Movie/Movie';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearQueueDetails,
fetchQueueDetails,
} from 'Store/Actions/queueActions';
import {
batchToggleMissingMovies,
clearMissing,
@ -42,8 +36,6 @@ import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import {
registerPagePopulator,
unregisterPagePopulator,
@ -120,8 +112,6 @@ function Missing() {
const isSearchingForMovies =
isSearchingForAllMovies || isSearchingForSelectedMovies;
const previousItems = usePrevious(items);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
@ -226,7 +216,6 @@ function Missing() {
return () => {
dispatch(clearMissing());
dispatch(clearQueueDetails());
};
}, [requestCurrentPage, dispatch]);
@ -246,16 +235,6 @@ function Missing() {
};
}, [dispatch]);
useEffect(() => {
if (!previousItems || hasDifferentItems(items, previousItems)) {
const movieIds = selectUniqueIds<Movie, number>(items, 'id');
if (movieIds.length) {
dispatch(fetchQueueDetails({ movieIds }));
}
}
}, [items, previousItems, dispatch]);
return (
<PageContent title={translate('Missing')}>
<PageToolbar>

View file

@ -131,7 +131,7 @@
"html-webpack-plugin": "5.6.0",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.9.1",
"postcss": "8.5.6",
"postcss": "8.4.47",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",

View file

@ -5,6 +5,8 @@
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
</packageSources>
</configuration>

View file

@ -122,7 +122,7 @@ private void MigrateAppDataFolder()
catch (Exception ex)
{
_logger.Debug(ex, ex.Message);
throw new RadarrStartupException(ex, "Unable to migrate DB from nzbdrone.db to {0}. Migrate manually", _appFolderInfo.GetDatabase());
throw new RadarrStartupException("Unable to migrate DB from nzbdrone.db to {0}. Migrate manually", _appFolderInfo.GetDatabase());
}
}
@ -199,7 +199,7 @@ private void MoveSqliteDatabase(string source, string destination)
private void RemovePidFile()
{
if (OsInfo.IsNotWindows && _diskProvider.FolderExists(_appFolderInfo.AppDataFolder))
if (OsInfo.IsNotWindows)
{
_diskProvider.DeleteFile(Path.Combine(_appFolderInfo.AppDataFolder, "radarr.pid"));
}

View file

@ -16,10 +16,9 @@
<PackageReference Include="Sentry" Version="4.0.2" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.2" />
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.ValueTuple" Version="4.6.1" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />

View file

@ -8,7 +8,6 @@
namespace NzbDrone.Core.Test.Http
{
[TestFixture]
[Platform(Exclude = "MacOsX")]
public class HttpProxySettingsProviderFixture : TestBase<HttpProxySettingsProvider>
{
private HttpProxySettings GetProxySettings()
@ -16,24 +15,24 @@ private HttpProxySettings GetProxySettings()
return new HttpProxySettings(ProxyType.Socks5, "localhost", 8080, "*.httpbin.org,google.com,172.16.0.0/12", true, null, null);
}
[TestCase("http://eu.httpbin.org/get")]
[TestCase("http://google.com/get")]
[TestCase("http://localhost:8654/get")]
[TestCase("http://172.21.0.1:8989/api/v3/indexer/schema")]
public void should_bypass_proxy(string url)
[Test]
public void should_bypass_proxy()
{
var settings = GetProxySettings();
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://eu.httpbin.org/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://google.com/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://localhost:8654/get")).Should().BeTrue();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.21.0.1:8989/api/v3/indexer/schema")).Should().BeTrue();
}
[TestCase("http://bing.com/get")]
[TestCase("http://172.3.0.1:8989/api/v3/indexer/schema")]
public void should_not_bypass_proxy(string url)
[Test]
public void should_not_bypass_proxy()
{
var settings = GetProxySettings();
Subject.ShouldProxyBeBypassed(settings, new HttpUri(url)).Should().BeFalse();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://bing.com/get")).Should().BeFalse();
Subject.ShouldProxyBeBypassed(settings, new HttpUri("http://172.3.0.1:8989/api/v3/indexer/schema")).Should().BeFalse();
}
}
}

View file

@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />

View file

@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using Polly;
using Polly.Retry;
namespace NzbDrone.Core.Datastore
{
@ -45,31 +40,12 @@ public interface IBasicRepository<TModel>
public class BasicRepository<TModel> : IBasicRepository<TModel>
where TModel : ModelBase, new()
{
private static readonly ILogger Logger = NzbDroneLogger.GetLogger(typeof(BasicRepository<TModel>));
private readonly IEventAggregator _eventAggregator;
private readonly PropertyInfo _keyProperty;
private readonly List<PropertyInfo> _properties;
private readonly string _updateSql;
private readonly string _insertSql;
private static ResiliencePipeline RetryStrategy => new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SQLiteException>(ex => ex.ResultCode == SQLiteErrorCode.Busy),
Delay = TimeSpan.FromMilliseconds(100),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
Logger.Warn(args.Outcome.Exception, "Failed writing to database. Retry #{0}", args.AttemptNumber);
return default;
}
})
.Build();
protected readonly IDatabase _database;
protected readonly string _table;
@ -210,9 +186,7 @@ private string GetInsertSql()
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
{
SqlBuilderExtensions.LogQuery(_insertSql, model);
var multi = RetryStrategy.Execute(static (state, _) => state.connection.QueryMultiple(state._insertSql, state.model, state.transaction), (connection, _insertSql, model, transaction));
var multi = connection.QueryMultiple(_insertSql, model, transaction);
var multiRead = multi.Read();
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
_keyProperty.SetValue(model, id);
@ -407,7 +381,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
SqlBuilderExtensions.LogQuery(sql, model);
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.model, transaction: state.transaction), (connection, sql, model, transaction));
connection.Execute(sql, model, transaction: transaction);
}
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
@ -419,7 +393,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
SqlBuilderExtensions.LogQuery(sql, model);
}
RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.models, transaction: state.transaction), (connection, sql, models, transaction));
connection.Execute(sql, models, transaction: transaction);
}
protected virtual SqlBuilder PagedBuilder() => Builder();

View file

@ -7,7 +7,7 @@
namespace NzbDrone.Core.Datastore.Migration
{
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
public class DatabaseEngineVersionCheck : ForwardOnlyMigration
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
{
protected readonly Logger _logger;
@ -22,6 +22,11 @@ public override void Up()
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
}
public override void Down()
{
// No-op
}
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
{
using (var versionCmd = conn.CreateCommand())

View file

@ -16,7 +16,7 @@ protected override void MainDbUpgrade()
if (!Schema.Table("ImportExclusions").Exists())
{
Create.TableForModel("ImportExclusions")
.WithColumn("TmdbId").AsInt64().NotNullable().Unique()
.WithColumn("TmdbId").AsInt64().NotNullable().Unique().PrimaryKey()
.WithColumn("MovieTitle").AsString().Nullable()
.WithColumn("MovieYear").AsInt64().Nullable().WithDefaultValue(0);
}

View file

@ -6,6 +6,7 @@
using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.Processors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
@ -19,10 +20,13 @@ public interface IMigrationController
public class MigrationController : IMigrationController
{
private readonly Logger _logger;
private readonly ILoggerProvider _migrationLoggerProvider;
public MigrationController(Logger logger)
public MigrationController(Logger logger,
ILoggerProvider migrationLoggerProvider)
{
_logger = logger;
_migrationLoggerProvider = migrationLoggerProvider;
}
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
@ -31,13 +35,16 @@ public void Migrate(string connectionString, MigrationContext migrationContext,
_logger.Info("*** Migrating {0} ***", connectionString);
ServiceProvider serviceProvider;
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
var serviceProvider = new ServiceCollection()
serviceProvider = new ServiceCollection()
.AddLogging(b => b.AddNLog())
.AddFluentMigratorCore()
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
.ConfigureRunner(builder => builder
.ConfigureRunner(
builder => builder
.AddPostgres()
.AddNzbDroneSQLite()
.WithGlobalConnectionString(connectionString)

View file

@ -4,14 +4,9 @@
using FluentMigrator.Builders.Create.Table;
using FluentMigrator.Runner;
using FluentMigrator.Runner.BatchParser;
using FluentMigrator.Runner.Generators;
using FluentMigrator.Runner.Generators.SQLite;
using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.Processors;
using FluentMigrator.Runner.Processors.SQLite;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
@ -31,40 +26,23 @@ public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction t
return command;
}
public static void AddParameter(this IDbCommand command, object value)
public static void AddParameter(this System.Data.IDbCommand command, object value)
{
var parameter = command.CreateParameter();
parameter.Value = value;
command.Parameters.Add(parameter);
}
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder, bool binaryGuid = false, bool useStrictTables = false)
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder)
{
builder.Services
.AddTransient<SQLiteBatchParser>()
.AddScoped<SQLiteDbFactory>()
.AddScoped<NzbDroneSQLiteProcessor>(sp =>
{
var factory = sp.GetService<SQLiteDbFactory>();
var logger = sp.GetService<ILogger<NzbDroneSQLiteProcessor>>();
var options = sp.GetService<IOptionsSnapshot<ProcessorOptions>>();
var connectionStringAccessor = sp.GetService<IConnectionStringAccessor>();
var sqliteQuoter = new SQLiteQuoter(false);
return new NzbDroneSQLiteProcessor(factory, sp.GetService<SQLiteGenerator>(), logger, options, connectionStringAccessor, sp, sqliteQuoter);
})
.AddScoped<ISQLiteTypeMap>(_ => new NzbDroneSQLiteTypeMap(useStrictTables))
.AddScoped<NzbDroneSQLiteProcessor>()
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
.AddScoped(
sp =>
{
var typeMap = sp.GetRequiredService<ISQLiteTypeMap>();
return new SQLiteGenerator(
new SQLiteQuoter(binaryGuid),
typeMap,
new OptionsWrapper<GeneratorOptions>(new GeneratorOptions()));
})
.AddScoped<SQLiteQuoter>()
.AddScoped<SQLiteGenerator>()
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
return builder;
}
}

View file

@ -15,8 +15,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class NzbDroneSQLiteProcessor : SQLiteProcessor
{
private readonly SQLiteQuoter _quoter;
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
SQLiteGenerator generator,
ILogger<NzbDroneSQLiteProcessor> logger,
@ -26,7 +24,6 @@ public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
SQLiteQuoter quoter)
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
{
_quoter = quoter;
}
public override void Process(AlterColumnExpression expression)
@ -38,7 +35,7 @@ public override void Process(AlterColumnExpression expression)
if (columnIndex == -1)
{
throw new ApplicationException($"Column {expression.Column.Name} does not exist on table {expression.TableName}.");
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
}
columnDefinitions[columnIndex] = expression.Column;
@ -48,28 +45,6 @@ public override void Process(AlterColumnExpression expression)
ProcessAlterTable(tableDefinition);
}
public override void Process(AlterDefaultConstraintExpression expression)
{
var tableDefinition = GetTableSchema(expression.TableName);
var columnDefinitions = tableDefinition.Columns.ToList();
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.ColumnName);
if (columnIndex == -1)
{
throw new ApplicationException($"Column {expression.ColumnName} does not exist on table {expression.TableName}.");
}
var changedColumn = columnDefinitions[columnIndex];
changedColumn.DefaultValue = expression.DefaultValue;
columnDefinitions[columnIndex] = changedColumn;
tableDefinition.Columns = columnDefinitions;
ProcessAlterTable(tableDefinition);
}
public override void Process(DeleteColumnExpression expression)
{
var tableDefinition = GetTableSchema(expression.TableName);
@ -87,7 +62,7 @@ public override void Process(DeleteColumnExpression expression)
if (columnsToRemove.Any())
{
throw new ApplicationException($"Column {columnsToRemove.First()} does not exist on table {expression.TableName}.");
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
}
ProcessAlterTable(tableDefinition);
@ -103,12 +78,12 @@ public override void Process(RenameColumnExpression expression)
if (columnIndex == -1)
{
throw new ApplicationException($"Column {expression.OldName} does not exist on table {expression.TableName}.");
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName));
}
if (columnDefinitions.Any(c => c.Name == expression.NewName))
{
throw new ApplicationException($"Column {expression.NewName} already exists on table {expression.TableName}.");
throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName));
}
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
@ -153,20 +128,21 @@ protected virtual void ProcessAlterTable(TableDefinition tableDefinition, List<C
}
// What is the cleanest way to do this? Add function to Generator?
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => _quoter.QuoteColumnName(c.Name)));
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => _quoter.QuoteColumnName(c.Name)));
var quoter = new SQLiteQuoter();
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name)));
Process(new CreateTableExpression { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
Process($"INSERT INTO {_quoter.QuoteTableName(tempTableName)} ({columnsToInsert}) SELECT {columnsToFetch} FROM {_quoter.QuoteTableName(tableName)}");
Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName)));
Process(new DeleteTableExpression { TableName = tableName });
Process(new DeleteTableExpression() { TableName = tableName });
Process(new RenameTableExpression { OldName = tempTableName, NewName = tableName });
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
foreach (var index in tableDefinition.Indexes)
{
Process(new CreateIndexExpression { Index = index });
Process(new CreateIndexExpression() { Index = index });
}
}
}

View file

@ -1,76 +0,0 @@
using System.Data;
using FluentMigrator.Runner.Generators.Base;
using FluentMigrator.Runner.Generators.SQLite;
namespace NzbDrone.Core.Datastore.Migration.Framework;
// Based on https://github.com/fluentmigrator/fluentmigrator/blob/v6.2.0/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteTypeMap.cs
public sealed class NzbDroneSQLiteTypeMap : TypeMapBase, ISQLiteTypeMap
{
public bool UseStrictTables { get; }
public NzbDroneSQLiteTypeMap(bool useStrictTables = false)
{
UseStrictTables = useStrictTables;
SetupTypeMaps();
}
// Must be kept in sync with upstream
protected override void SetupTypeMaps()
{
SetTypeMap(DbType.Binary, "BLOB");
SetTypeMap(DbType.Byte, "INTEGER");
SetTypeMap(DbType.Int16, "INTEGER");
SetTypeMap(DbType.Int32, "INTEGER");
SetTypeMap(DbType.Int64, "INTEGER");
SetTypeMap(DbType.SByte, "INTEGER");
SetTypeMap(DbType.UInt16, "INTEGER");
SetTypeMap(DbType.UInt32, "INTEGER");
SetTypeMap(DbType.UInt64, "INTEGER");
if (!UseStrictTables)
{
SetTypeMap(DbType.Currency, "NUMERIC");
SetTypeMap(DbType.Decimal, "NUMERIC");
SetTypeMap(DbType.Double, "NUMERIC");
SetTypeMap(DbType.Single, "NUMERIC");
SetTypeMap(DbType.VarNumeric, "NUMERIC");
SetTypeMap(DbType.Date, "DATETIME");
SetTypeMap(DbType.DateTime, "DATETIME");
SetTypeMap(DbType.DateTime2, "DATETIME");
SetTypeMap(DbType.Time, "DATETIME");
SetTypeMap(DbType.Guid, "UNIQUEIDENTIFIER");
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
SetTypeMap(DbType.DateTimeOffset, "DATETIME");
}
else
{
SetTypeMap(DbType.Currency, "TEXT");
SetTypeMap(DbType.Decimal, "TEXT");
SetTypeMap(DbType.Double, "REAL");
SetTypeMap(DbType.Single, "REAL");
SetTypeMap(DbType.VarNumeric, "TEXT");
SetTypeMap(DbType.Date, "TEXT");
SetTypeMap(DbType.DateTime, "TEXT");
SetTypeMap(DbType.DateTime2, "TEXT");
SetTypeMap(DbType.Time, "TEXT");
SetTypeMap(DbType.Guid, "TEXT");
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
SetTypeMap(DbType.DateTimeOffset, "TEXT");
}
SetTypeMap(DbType.AnsiString, "TEXT");
SetTypeMap(DbType.String, "TEXT");
SetTypeMap(DbType.AnsiStringFixedLength, "TEXT");
SetTypeMap(DbType.StringFixedLength, "TEXT");
SetTypeMap(DbType.Boolean, "INTEGER");
}
public override string GetTypeMap(DbType type, int? size, int? precision)
{
return base.GetTypeMap(type, size: null, precision: null);
}
}

View file

@ -424,8 +424,8 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSe
}
catch (HttpException ex)
{
_logger.Debug(ex, "qbitTorrent authentication failed.");
if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
_logger.Debug("qbitTorrent authentication failed.");
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
{
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
}
@ -437,7 +437,7 @@ private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSe
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
}
if (response.Content.IsNotNullOrWhiteSpace() && response.Content != "Ok.")
if (response.Content != "Ok.")
{
// returns "Fails." on bad login
_logger.Debug("qbitTorrent authentication failed.");

View file

@ -221,7 +221,7 @@ private string DownloadFromMagnetUrl(RemoteMovie remoteMovie, IIndexer indexer,
try
{
hash = MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
hash = MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
}
catch (FormatException ex)
{

View file

@ -82,7 +82,7 @@ protected virtual string GetInfoHash(XElement item)
{
try
{
return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
return MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
}
catch
{

View file

@ -1340,13 +1340,11 @@
"NotificationsPushoverSettingsDevices": "Devices",
"NotificationsPushoverSettingsDevicesHelpText": "List of device names (leave blank to send to all devices)",
"NotificationsPushoverSettingsExpire": "Expire",
"NotificationsPushoverSettingsExpireHelpText": "Maximum time to retry Emergency alerts, maximum 86400 seconds",
"NotificationsPushoverSettingsExpireHelpText": "Maximum time to retry Emergency alerts, maximum 86400 seconds\"",
"NotificationsPushoverSettingsRetry": "Retry",
"NotificationsPushoverSettingsRetryHelpText": "Interval to retry Emergency alerts, minimum 30 seconds",
"NotificationsPushoverSettingsSound": "Sound",
"NotificationsPushoverSettingsSoundHelpText": "Notification sound, leave blank to use the default",
"NotificationsPushoverSettingsTtl": "Time To Live",
"NotificationsPushoverSettingsTtlHelpText": "Time in seconds before the message expires. Set to 0 for unlimited duration",
"NotificationsPushoverSettingsUserKey": "User Key",
"NotificationsSendGridSettingsApiKeyHelpText": "The API Key generated by SendGrid",
"NotificationsSettingsUpdateLibrary": "Update Library",

View file

@ -22,7 +22,7 @@ public string GetHashFromTorrentFile(byte[] fileContents)
{
try
{
return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex();
return Torrent.Load(fileContents).InfoHash.ToHex();
}
catch
{

View file

@ -45,11 +45,6 @@ public void SendNotification(string title, string message, PushoverSettings sett
requestBuilder.AddFormParameter("expire", settings.Expire);
}
if (settings.Ttl > 0)
{
requestBuilder.AddFormParameter("ttl", settings.Ttl);
}
if (!settings.Sound.IsNullOrWhiteSpace())
{
requestBuilder.AddFormParameter("sound", settings.Sound);

View file

@ -13,7 +13,6 @@ public PushoverSettingsValidator()
RuleFor(c => c.UserKey).NotEmpty();
RuleFor(c => c.Retry).GreaterThanOrEqualTo(30).LessThanOrEqualTo(86400).When(c => (PushoverPriority)c.Priority == PushoverPriority.Emergency);
RuleFor(c => c.Retry).GreaterThanOrEqualTo(0).LessThanOrEqualTo(86400).When(c => (PushoverPriority)c.Priority == PushoverPriority.Emergency);
RuleFor(c => c.Ttl).GreaterThanOrEqualTo(0);
}
}
@ -45,10 +44,7 @@ public PushoverSettings()
[FieldDefinition(5, Label = "NotificationsPushoverSettingsExpire", Type = FieldType.Textbox, HelpText = "NotificationsPushoverSettingsExpireHelpText")]
public int Expire { get; set; }
[FieldDefinition(6, Label = "NotificationsPushoverSettingsTtl", Type = FieldType.Textbox, HelpText = "NotificationsPushoverSettingsTtlHelpText", Advanced = true)]
public int Ttl { get; set; }
[FieldDefinition(7, Label = "NotificationsPushoverSettingsSound", Type = FieldType.Textbox, HelpText = "NotificationsPushoverSettingsSoundHelpText", HelpLink = "https://pushover.net/api#sounds")]
[FieldDefinition(6, Label = "NotificationsPushoverSettingsSound", Type = FieldType.Textbox, HelpText = "NotificationsPushoverSettingsSoundHelpText", HelpLink = "https://pushover.net/api#sounds")]
public string Sound { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(UserKey) && Priority >= -1 && Priority <= 2;

View file

@ -13,19 +13,19 @@
<PackageReference Include="Polly" Version="8.6.0" />
<PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" />
<PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" />
<PackageReference Include="System.Drawing.Common" Version="8.0.20" />
<PackageReference Include="System.Memory" Version="4.6.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="FluentMigrator.Runner.Core" Version="6.2.0" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="6.2.0" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="6.2.0" />
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="MonoTorrent" Version="2.0.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using DryIoc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@ -33,7 +32,6 @@
using Radarr.Http.ErrorManagement;
using Radarr.Http.Frontend;
using Radarr.Http.Middleware;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
@ -62,11 +60,8 @@ public void ConfigureServices(IServiceCollection services)
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fc00::"), 7));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("fe80::"), 10));
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddRouting(options => options.LowercaseUrls = true);

View file

@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="RestSharp" Version="106.15.0" />

View file

@ -4875,10 +4875,10 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.11, nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
natural-compare@^1.4.0:
version "1.4.0"
@ -5211,7 +5211,12 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1:
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@ -5397,13 +5402,13 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.5.6:
version "8.5.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
postcss@8.4.47, postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.32:
version "8.4.47"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
nanoid "^3.3.7"
picocolors "^1.1.0"
source-map-js "^1.2.1"
postcss@^6.0.23:
@ -5415,15 +5420,6 @@ postcss@^6.0.23:
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^8.0.0, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.32:
version "8.5.0"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.0.tgz#15244b9fd65f809b2819682456f0e7e1e30c145b"
integrity sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prefix-style@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06"