mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
19 Commits
v0.1.4.115
...
v0.1.6.118
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c83c818380 | ||
|
|
a2df38b1ca | ||
|
|
89510c4a65 | ||
|
|
b5a2f68bde | ||
|
|
1ffab661da | ||
|
|
bf0a627a4e | ||
|
|
df764ce8b4 | ||
|
|
a61d4ab88c | ||
|
|
01e7e924c4 | ||
|
|
5f5df99dab | ||
|
|
77e40e8e53 | ||
|
|
d3853c1a54 | ||
|
|
02ecc04526 | ||
|
|
2e103d6dba | ||
|
|
9b9d2f2798 | ||
|
|
b80bfaeff0 | ||
|
|
80b6821e46 | ||
|
|
dbf81a7c9e | ||
|
|
cb2e5953d5 |
@@ -7,7 +7,7 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.4'
|
||||
majorVersion: '0.1.6'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
@@ -163,7 +163,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -816,7 +815,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
|
||||
@@ -7,8 +7,10 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import StatsFilterMenu from './StatsFilterMenu';
|
||||
import styles from './Stats.css';
|
||||
|
||||
function getAverageResponseTimeData(indexerStats) {
|
||||
@@ -144,14 +146,29 @@ function Stats(props) {
|
||||
item,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
error,
|
||||
filters,
|
||||
selectedFilterKey,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
const isLoaded = !!(!error && isPopulated);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar />
|
||||
<PageToolbar>
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
<StatsFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
@@ -232,6 +249,10 @@ Stats.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerStats } from 'Store/Actions/indexerStatsActions';
|
||||
import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
import Stats from './Stats';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -12,9 +12,16 @@ function createMapStateToProps() {
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexerStats
|
||||
};
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setIndexerStatsFilter({ selectedFilterKey }));
|
||||
},
|
||||
dispatchFetchIndexerStats() {
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class StatsConnector extends Component {
|
||||
|
||||
@@ -22,7 +29,7 @@ class StatsConnector extends Component {
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchIndexers();
|
||||
this.props.dispatchFetchIndexerStats();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -38,7 +45,7 @@ class StatsConnector extends Component {
|
||||
}
|
||||
|
||||
StatsConnector.propTypes = {
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
dispatchFetchIndexerStats: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(StatsConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);
|
||||
|
||||
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
37
frontend/src/Indexer/Stats/StatsFilterMenu.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function StatsFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
isDisabled,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={isDisabled}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StatsFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
StatsFilterMenu.defaultProps = {
|
||||
showCustomFilters: false
|
||||
};
|
||||
|
||||
export default StatsFilterMenu;
|
||||
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
24
frontend/src/Indexer/Stats/StatsFilterModalConnector.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.indexerStats.items,
|
||||
(state) => state.indexerStats.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'indexerStats'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setIndexerStatsFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
@@ -14,7 +14,6 @@ function createRemoveItemHandler(section, url) {
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/${id}?${$.param(queryParams, true)}`,
|
||||
dataType: 'text',
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
@@ -15,30 +20,140 @@ export const defaultState = {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: {},
|
||||
start: null,
|
||||
end: null,
|
||||
|
||||
details: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: []
|
||||
}
|
||||
},
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastSeven',
|
||||
label: 'Last 7 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastThirty',
|
||||
label: 'Last 30 Days',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'lastNinety',
|
||||
label: 'Last 90 Days',
|
||||
filters: []
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'startDate',
|
||||
label: 'Start Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.DATE
|
||||
}
|
||||
],
|
||||
selectedFilterKey: 'all'
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'indexerStats.customFilters',
|
||||
'indexerStats.selectedFilterKey'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
|
||||
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerStats = createThunk(FETCH_INDEXER_STATS);
|
||||
export const setIndexerStatsFilter = createThunk(SET_INDEXER_STATS_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_INDEXER_STATS]: createFetchHandler(section, '/indexerStats')
|
||||
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const indexerStats = state.indexerStats;
|
||||
|
||||
const requestParams = {
|
||||
endDate: moment().toISOString()
|
||||
};
|
||||
|
||||
if (indexerStats.selectedFilterKey !== 'all') {
|
||||
let dayCount = 7;
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastThirty') {
|
||||
dayCount = 30;
|
||||
}
|
||||
|
||||
if (indexerStats.selectedFilterKey === 'lastNinety') {
|
||||
dayCount = 90;
|
||||
}
|
||||
|
||||
requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString();
|
||||
}
|
||||
|
||||
const basesAttrs = {
|
||||
section,
|
||||
isFetching: true
|
||||
};
|
||||
|
||||
const attrs = basesAttrs;
|
||||
|
||||
dispatch(set(attrs));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/indexerStats',
|
||||
data: requestParams
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[SET_INDEXER_STATS_FILTER]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, ...payload }));
|
||||
dispatch(fetchIndexerStats());
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -20,10 +20,11 @@ class About extends Component {
|
||||
packageVersion,
|
||||
packageAuthor,
|
||||
isNetCore,
|
||||
isMono,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -48,14 +49,6 @@ class About extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMono &&
|
||||
<DescriptionListItem
|
||||
title={translate('MonoVersion')}
|
||||
data={runtimeVersion}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isNetCore &&
|
||||
<DescriptionListItem
|
||||
@@ -77,6 +70,11 @@ class About extends Component {
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
@@ -114,9 +112,10 @@ About.propTypes = {
|
||||
packageVersion: PropTypes.string,
|
||||
packageAuthor: PropTypes.string,
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
isMono: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
|
||||
@@ -3,6 +3,8 @@ using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -25,12 +27,15 @@ namespace NzbDrone.Common.Test
|
||||
.AddNzbDroneLogger()
|
||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||
.AddDummyDatabase()
|
||||
.AddStartupContext(new StartupContext("first", "second"))
|
||||
.GetServiceProvider();
|
||||
.AddStartupContext(new StartupContext("first", "second"));
|
||||
|
||||
container.GetRequiredService<IAppFolderFactory>().Register();
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
|
||||
Mocker.SetConstant<System.IServiceProvider>(container);
|
||||
var serviceProvider = container.GetServiceProvider();
|
||||
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
||||
Mocker.SetConstant<System.IServiceProvider>(serviceProvider);
|
||||
|
||||
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
|
||||
.Select(c => c.GetType().FullName);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Security.Principal;
|
||||
using System.ServiceProcess;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Hosting.WindowsServices;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Processes;
|
||||
|
||||
@@ -14,14 +14,11 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
private readonly Logger _logger;
|
||||
private readonly DateTime _startTime = DateTime.UtcNow;
|
||||
|
||||
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
|
||||
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
IsWindowsService = !IsUserInteractive &&
|
||||
OsInfo.IsWindows &&
|
||||
serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) &&
|
||||
serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending;
|
||||
IsWindowsService = hostLifetime is WindowsServiceLifetime;
|
||||
|
||||
// net6.0 will return Radarr.dll for entry assembly, we need the actual
|
||||
// executable name (Radarr on linux). On mono this will return the location of
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
<PackageReference Include="DryIoc.dll" Version="4.8.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-12" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
|
||||
<ApplicationIcon>..\NzbDrone.Host\Prowlarr.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="!$(RuntimeIdentifier.StartsWith('win'))">
|
||||
<AssemblyName>Prowlarr</AssemblyName>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.net>
|
||||
<connectionManagement>
|
||||
<add address="*" maxconnection="100" />
|
||||
</connectionManagement>
|
||||
</system.net>
|
||||
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<loadFromRemoteSources enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -20,5 +20,19 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
{
|
||||
ParseUtil.GetBytes(stringSize).Should().Be(size);
|
||||
}
|
||||
|
||||
[TestCase(" some string ", "some string")]
|
||||
public void should_normalize_multiple_spaces(string original, string newString)
|
||||
{
|
||||
ParseUtil.NormalizeMultiSpaces(original).Should().Be(newString);
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("11", 11)]
|
||||
[TestCase("1000 grabs", 1000)]
|
||||
public void should_parse_int_from_string(string original, int parsedInt)
|
||||
{
|
||||
ParseUtil.CoerceInt(original).Should().Be(parsedInt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-12" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -47,6 +47,12 @@ namespace NzbDrone.Core.Configuration
|
||||
string UpdateScriptPath { get; }
|
||||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
@@ -186,6 +192,12 @@ namespace NzbDrone.Core.Configuration
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false);
|
||||
public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false);
|
||||
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
|
||||
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
||||
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
||||
public int PostgresPort => GetValueInt("PostgresPort", 5436, persist: false);
|
||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}");
|
||||
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM \"{_table}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,22 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
|
||||
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
||||
if (_database.DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
|
||||
}
|
||||
}
|
||||
|
||||
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
|
||||
{
|
||||
SqlBuilderExtensions.LogQuery(_insertSql, model);
|
||||
var multi = connection.QueryMultiple(_insertSql, model, transaction);
|
||||
var id = (int)multi.Read().First().id;
|
||||
var multiRead = multi.Read();
|
||||
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||
_keyProperty.SetValue(model, id);
|
||||
|
||||
_database.ApplyLazyLoad(model);
|
||||
@@ -287,7 +295,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
conn.Execute($"DELETE FROM [{_table}]");
|
||||
conn.Execute($"DELETE FROM \"{_table}\"");
|
||||
}
|
||||
|
||||
if (vacuum)
|
||||
@@ -346,7 +354,7 @@ namespace NzbDrone.Core.Datastore
|
||||
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("UPDATE {0} SET ", _table);
|
||||
sb.AppendFormat("UPDATE \"{0}\" SET ", _table);
|
||||
|
||||
for (var i = 0; i < propertiesToUpdate.Count; i++)
|
||||
{
|
||||
@@ -414,9 +422,12 @@ namespace NzbDrone.Core.Datastore
|
||||
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
||||
}
|
||||
|
||||
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
|
||||
|
||||
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
||||
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
return queryFunc(builder).ToList();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
@@ -14,10 +16,17 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public class ConnectionStringFactory : IConnectionStringFactory
|
||||
{
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
|
||||
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
_configFileProvider = configFileProvider;
|
||||
|
||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||
GetConnectionString(appFolderInfo.GetDatabase());
|
||||
|
||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
}
|
||||
|
||||
public string MainDbConnectionString { get; private set; }
|
||||
@@ -48,5 +57,19 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
}
|
||||
|
||||
private string GetPostgresConnectionString(string dbName)
|
||||
{
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder();
|
||||
|
||||
connectionBuilder.Database = dbName;
|
||||
connectionBuilder.Host = _configFileProvider.PostgresHost;
|
||||
connectionBuilder.Username = _configFileProvider.PostgresUser;
|
||||
connectionBuilder.Password = _configFileProvider.PostgresPassword;
|
||||
connectionBuilder.Port = _configFileProvider.PostgresPort;
|
||||
connectionBuilder.Enlist = false;
|
||||
|
||||
return connectionBuilder.ConnectionString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.Datastore
|
||||
IDbConnection OpenConnection();
|
||||
Version Version { get; }
|
||||
int Migration { get; }
|
||||
DatabaseType DatabaseType { get; }
|
||||
void Vacuum();
|
||||
}
|
||||
|
||||
@@ -32,13 +34,44 @@ namespace NzbDrone.Core.Datastore
|
||||
return _datamapperFactory();
|
||||
}
|
||||
|
||||
public DatabaseType DatabaseType
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version Version
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
var version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
string version;
|
||||
|
||||
try
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SHOW server_version");
|
||||
|
||||
//Postgres can return extra info about operating system on version call, ignore this
|
||||
version = Regex.Replace(version, @"\(.*?\)", "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
|
||||
}
|
||||
|
||||
return new Version(version);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +83,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
return db.QueryFirstOrDefault<int>("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1");
|
||||
return db.QueryFirstOrDefault<int>("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,4 +106,10 @@ namespace NzbDrone.Core.Datastore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DatabaseType
|
||||
{
|
||||
SQLite,
|
||||
PostgreSQL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@@ -85,10 +88,19 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
var db = new Database(migrationContext.MigrationType.ToString(), () =>
|
||||
{
|
||||
var conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
conn.Open();
|
||||
DbConnection conn;
|
||||
|
||||
if (connectionString.Contains(".db"))
|
||||
{
|
||||
conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn = new NpgsqlConnection(connectionString);
|
||||
}
|
||||
|
||||
conn.Open();
|
||||
return conn;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public static SqlBuilder Select(this SqlBuilder builder, params Type[] types)
|
||||
{
|
||||
return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", "));
|
||||
return builder.Select(types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", "));
|
||||
}
|
||||
|
||||
public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
|
||||
public class LogDatabase : ILogDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public LogDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
@@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
|
||||
public class MainDatabase : IMainDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public MainDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
@@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("UPDATE Notifications SET Implementation = Replace(Implementation, 'DiscordNotifier', 'Notifiarr'),ConfigContract = Replace(ConfigContract, 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE Implementation = 'DiscordNotifier';");
|
||||
Execute.Sql("UPDATE \"Notifications\" SET \"Implementation\" = Replace(\"Implementation\", 'DiscordNotifier', 'Notifiarr'),\"ConfigContract\" = Replace(\"ConfigContract\", 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE \"Implementation\" = 'DiscordNotifier';");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
Alter.Table("History")
|
||||
.AddColumn("Successful").AsBoolean().NotNullable().WithDefaultValue(true);
|
||||
|
||||
Execute.Sql("UPDATE History SET Successful = (json_extract(History.Data,'$.successful') == 'True' );");
|
||||
// Postgres added after this, not needed
|
||||
IfDatabase("sqlite").Execute.Sql("UPDATE \"History\" SET \"Successful\" = (json_extract(\"History\".\"Data\",'$.successful') == 'True' );");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT Id, Settings FROM Indexers WHERE Implementation = 'Redacted'";
|
||||
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Redacted'";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
using (var updateCmd = conn.CreateCommand())
|
||||
{
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = "UPDATE Indexers SET Settings = ?, ConfigContract = ?, Enable = 0 WHERE Id = ?";
|
||||
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?";
|
||||
updateCmd.AddParameter(settings);
|
||||
updateCmd.AddParameter("RedactedSettings");
|
||||
updateCmd.AddParameter(id);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "Unit3dSettings", Enable = 0 }).Where(new { Implementation = "DesiTorrents" });
|
||||
Update.Table("Indexers").Set(new { ConfigContract = "Unit3dSettings", Enable = true }).Where(new { Implementation = "DesiTorrents" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using FluentMigrator.Runner;
|
||||
using FluentMigrator.Runner.Generators;
|
||||
using FluentMigrator.Runner.Initialization;
|
||||
using FluentMigrator.Runner.Processors;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -34,11 +35,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
|
||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||
|
||||
var serviceProvider = new ServiceCollection()
|
||||
ServiceProvider serviceProvider;
|
||||
|
||||
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
|
||||
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
.AddFluentMigratorCore()
|
||||
.ConfigureRunner(
|
||||
builder => builder
|
||||
.AddPostgres()
|
||||
.AddNzbDroneSQLite()
|
||||
.WithGlobalConnectionString(connectionString)
|
||||
.WithMigrationsIn(Assembly.GetExecutingAssembly()))
|
||||
@@ -48,6 +54,14 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||
opt.PreviewOnly = false;
|
||||
opt.Timeout = TimeSpan.FromSeconds(60);
|
||||
})
|
||||
.Configure<SelectingProcessorAccessorOptions>(cfg =>
|
||||
{
|
||||
cfg.ProcessorId = db;
|
||||
})
|
||||
.Configure<SelectingGeneratorAccessorOptions>(cfg =>
|
||||
{
|
||||
cfg.GeneratorId = db;
|
||||
})
|
||||
.BuildServiceProvider();
|
||||
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class TableMapper
|
||||
{
|
||||
private readonly HashSet<string> _allowedOrderBy = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public TableMapper()
|
||||
{
|
||||
IgnoreList = new Dictionary<Type, List<PropertyInfo>>();
|
||||
@@ -27,12 +29,12 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
if (IgnoreList.TryGetValue(type, out var list))
|
||||
{
|
||||
return new ColumnMapper<TEntity>(list, LazyLoadList[type]);
|
||||
return new ColumnMapper<TEntity>(list, LazyLoadList[type], _allowedOrderBy);
|
||||
}
|
||||
|
||||
IgnoreList[type] = new List<PropertyInfo>();
|
||||
LazyLoadList[type] = new List<LazyLoadedProperty>();
|
||||
return new ColumnMapper<TEntity>(IgnoreList[type], LazyLoadList[type]);
|
||||
return new ColumnMapper<TEntity>(IgnoreList[type], LazyLoadList[type], _allowedOrderBy);
|
||||
}
|
||||
|
||||
public List<PropertyInfo> ExcludeProperties(Type x)
|
||||
@@ -40,6 +42,64 @@ namespace NzbDrone.Core.Datastore
|
||||
return IgnoreList.ContainsKey(x) ? IgnoreList[x] : new List<PropertyInfo>();
|
||||
}
|
||||
|
||||
public bool IsValidSortKey(string sortKey)
|
||||
{
|
||||
string table = null;
|
||||
|
||||
if (sortKey.Contains('.'))
|
||||
{
|
||||
var split = sortKey.Split('.');
|
||||
if (split.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
table = split[0];
|
||||
sortKey = split[1];
|
||||
}
|
||||
|
||||
if (table != null && !TableMap.Values.Contains(table, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_allowedOrderBy.Contains(sortKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetSortKey(string sortKey)
|
||||
{
|
||||
string table = null;
|
||||
|
||||
if (sortKey.Contains('.'))
|
||||
{
|
||||
var split = sortKey.Split('.');
|
||||
if (split.Length != 2)
|
||||
{
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
table = split[0];
|
||||
sortKey = split[1];
|
||||
}
|
||||
|
||||
if (table != null && !TableMap.Values.Contains(table, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
if (!_allowedOrderBy.Contains(sortKey))
|
||||
{
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
return _allowedOrderBy.First(x => x.Equals(sortKey, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public string TableNameMapping(Type x)
|
||||
{
|
||||
return TableMap.ContainsKey(x) ? TableMap[x] : null;
|
||||
@@ -47,17 +107,17 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
public string SelectTemplate(Type x)
|
||||
{
|
||||
return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
}
|
||||
|
||||
public string DeleteTemplate(Type x)
|
||||
{
|
||||
return $"DELETE FROM {TableMap[x]} /**where**/";
|
||||
return $"DELETE FROM \"{TableMap[x]}\" /**where**/";
|
||||
}
|
||||
|
||||
public string PageCountTemplate(Type x)
|
||||
{
|
||||
return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
|
||||
return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,17 +132,20 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
private readonly List<PropertyInfo> _ignoreList;
|
||||
private readonly List<LazyLoadedProperty> _lazyLoadList;
|
||||
private readonly HashSet<string> _allowedOrderBy;
|
||||
|
||||
public ColumnMapper(List<PropertyInfo> ignoreList, List<LazyLoadedProperty> lazyLoadList)
|
||||
public ColumnMapper(List<PropertyInfo> ignoreList, List<LazyLoadedProperty> lazyLoadList, HashSet<string> allowedOrderBy)
|
||||
{
|
||||
_ignoreList = ignoreList;
|
||||
_lazyLoadList = lazyLoadList;
|
||||
_allowedOrderBy = allowedOrderBy;
|
||||
}
|
||||
|
||||
public ColumnMapper<T> AutoMapPropertiesWhere(Func<PropertyInfo, bool> predicate)
|
||||
{
|
||||
var properties = typeof(T).GetProperties();
|
||||
_ignoreList.AddRange(properties.Where(x => !predicate(x)));
|
||||
_allowedOrderBy.UnionWith(properties.Where(x => predicate(x)).Select(x => x.Name));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerObsoleteCheckMessage"),
|
||||
string.Join(", ", oldIndexers.Select(v => v.Name))),
|
||||
string.Join(", ", oldIndexers.Select(v => v.Definition.Name))),
|
||||
"#indexers-are-obsolete");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
|
||||
void DeleteForIndexers(List<int> indexerIds);
|
||||
History MostRecentForIndexer(int indexerId);
|
||||
List<History> Between(DateTime start, DateTime end);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
void Cleanup(int days);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
@@ -78,6 +79,13 @@ namespace NzbDrone.Core.History
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public List<History> Between(DateTime start, DateTime end)
|
||||
{
|
||||
var builder = Builder().Where<History>(x => x.Date >= start && x.Date <= end);
|
||||
|
||||
return Query(builder).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
|
||||
public List<History> Since(DateTime date, HistoryEventType? eventType)
|
||||
{
|
||||
var builder = Builder().Where<History>(x => x.Date >= date);
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> FindByDownloadId(string downloadId);
|
||||
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
|
||||
void UpdateMany(List<History> toUpdate);
|
||||
List<History> Between(DateTime start, DateTime end);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
}
|
||||
@@ -87,6 +88,11 @@ namespace NzbDrone.Core.History
|
||||
_historyRepository.UpdateMany(toUpdate);
|
||||
}
|
||||
|
||||
public List<History> Between(DateTime start, DateTime end)
|
||||
{
|
||||
return _historyRepository.Between(start, end);
|
||||
}
|
||||
|
||||
public List<History> Since(DateTime date, HistoryEventType? eventType)
|
||||
{
|
||||
return _historyRepository.Since(date, eventType);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
@@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM Users
|
||||
WHERE ID NOT IN (
|
||||
SELECT ID FROM Users
|
||||
mapper.Execute(@"DELETE FROM ""Users""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""Users""
|
||||
LIMIT 1)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
mapper.Execute(@"DELETE FROM DownloadClientStatus
|
||||
WHERE Id IN (
|
||||
SELECT DownloadClientStatus.Id FROM DownloadClientStatus
|
||||
LEFT OUTER JOIN DownloadClients
|
||||
ON DownloadClientStatus.ProviderId = DownloadClients.Id
|
||||
WHERE DownloadClients.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
|
||||
LEFT OUTER JOIN ""DownloadClients""
|
||||
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
|
||||
WHERE ""DownloadClients"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM History
|
||||
WHERE Id IN (
|
||||
SELECT History.Id FROM History
|
||||
LEFT OUTER JOIN Indexers
|
||||
ON History.IndexerId = Indexers.Id
|
||||
WHERE Indexers.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""History"".""IndexerId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM IndexerStatus
|
||||
WHERE Id IN (
|
||||
SELECT IndexerStatus.Id FROM IndexerStatus
|
||||
LEFT OUTER JOIN Indexers
|
||||
ON IndexerStatus.ProviderId = Indexers.Id
|
||||
WHERE Indexers.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""IndexerStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,22 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
|
||||
if (usedTags.Length > 0)
|
||||
{
|
||||
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
|
||||
|
||||
mapper.Execute($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})");
|
||||
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute($"DELETE FROM \"Tags\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] GetUsedTags(string table, IDbConnection mapper)
|
||||
{
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'")
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]'")
|
||||
.SelectMany(x => x)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -26,9 +26,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"UPDATE ScheduledTasks
|
||||
SET LastExecution = @time
|
||||
WHERE LastExecution > @time",
|
||||
mapper.Execute(@"UPDATE ""ScheduledTasks""
|
||||
SET ""LastExecution"" = @time
|
||||
WHERE ""LastExecution"" > @time",
|
||||
new { time = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
|
||||
public override HttpRequest PreRequest(HttpRequest request)
|
||||
{
|
||||
//Try original request first, detect CF in post response
|
||||
//Try original request first, ignore errors, detect CF in post response
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var request = PreRequest(_cloudRequestBuilder.Create()
|
||||
var request = GenerateFlareSolverrRequest(_cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build());
|
||||
|
||||
@@ -157,12 +159,13 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
|
||||
{
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
// We only care about 400 responses, other error codes can be ignored
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
_logger.Error("Proxy Health Check failed: {0}", response.StatusCode);
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckBadRequestMessage"), response.StatusCode)));
|
||||
}
|
||||
|
||||
var result = JsonConvert.DeserializeObject<FlareSolverrResponse>(response.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -31,12 +31,6 @@ namespace NzbDrone.Core.IndexerProxies
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
var addresses = Dns.GetHostAddresses(Settings.Host);
|
||||
if (!addresses.Any())
|
||||
{
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format(_localizationService.GetLocalizedString("ProxyCheckResolveIpMessage"), addresses)));
|
||||
}
|
||||
|
||||
var request = PreRequest(_cloudRequestBuilder.Create()
|
||||
.Resource("/ping")
|
||||
.Build());
|
||||
@@ -55,7 +49,7 @@ namespace NzbDrone.Core.IndexerProxies
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Proxy Health Check failed");
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", request.Url)));
|
||||
failures.Add(new NzbDroneValidationFailure("Host", string.Format("Failed to test proxy: {0}", ex.Message)));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
r.Categories == null ? null : from c in r.Categories select GetNabElement("category", c.Id, protocol),
|
||||
r.IndexerFlags == null ? null : from f in r.IndexerFlags select GetNabElement("tag", f.Name, protocol),
|
||||
GetNabElement("rageid", r.TvRageId, protocol),
|
||||
GetNabElement("thetvdb", r.TvdbId, protocol),
|
||||
GetNabElement("tvdbid", r.TvdbId, protocol),
|
||||
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
|
||||
GetNabElement("tmdb", r.TmdbId, protocol),
|
||||
GetNabElement("seeders", t.Seeders, protocol),
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
public class CombinedStatistics
|
||||
{
|
||||
public List<IndexerStatistics> IndexerStatistics { get; set; }
|
||||
public List<UserAgentStatistics> UserAgentStatistics { get; set; }
|
||||
public List<HostStatistics> HostStatistics { get; set; }
|
||||
}
|
||||
|
||||
public class IndexerStatistics : ResultSet
|
||||
{
|
||||
public int IndexerId { get; set; }
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
public interface IIndexerStatisticsRepository
|
||||
{
|
||||
List<IndexerStatistics> IndexerStatistics();
|
||||
List<UserAgentStatistics> UserAgentStatistics();
|
||||
List<HostStatistics> HostStatistics();
|
||||
}
|
||||
|
||||
public class IndexerStatisticsRepository : IIndexerStatisticsRepository
|
||||
{
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM History /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
|
||||
private readonly IMainDatabase _database;
|
||||
|
||||
public IndexerStatisticsRepository(IMainDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public List<IndexerStatistics> IndexerStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return Query(IndexerBuilder());
|
||||
}
|
||||
|
||||
public List<UserAgentStatistics> UserAgentStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return UserAgentQuery(UserAgentBuilder());
|
||||
}
|
||||
|
||||
public List<HostStatistics> HostStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return HostQuery(HostBuilder());
|
||||
}
|
||||
|
||||
private List<IndexerStatistics> Query(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<IndexerStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<UserAgentStatistics> UserAgentQuery(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<UserAgentStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<HostStatistics> HostQuery(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<HostStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private SqlBuilder IndexerBuilder() => new SqlBuilder()
|
||||
.Select(@"Indexers.Id AS IndexerId,
|
||||
Indexers.Name AS IndexerName,
|
||||
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 2 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedQueries,
|
||||
SUM(CASE WHEN EventType == 3 then 1 else 0 end) AS NumberOfRssQueries,
|
||||
SUM(CASE WHEN EventType == 3 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedRssQueries,
|
||||
SUM(CASE WHEN EventType == 4 then 1 else 0 end) AS NumberOfAuthQueries,
|
||||
SUM(CASE WHEN EventType == 4 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedAuthQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs,
|
||||
SUM(CASE WHEN EventType == 1 AND Successful == 0 then 1 else 0 end) AS NumberOfFailedGrabs,
|
||||
AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime")
|
||||
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
|
||||
.GroupBy<IndexerDefinition>(x => x.Id);
|
||||
|
||||
private SqlBuilder UserAgentBuilder() => new SqlBuilder()
|
||||
.Select(@"json_extract(History.Data,'$.source') AS UserAgent,
|
||||
SUM(CASE WHEN EventType == 2 OR EventType == 3 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
|
||||
.GroupBy("UserAgent");
|
||||
|
||||
private SqlBuilder HostBuilder() => new SqlBuilder()
|
||||
.Select(@"json_extract(History.Data,'$.host') AS Host,
|
||||
SUM(CASE WHEN EventType == 2 OR EventType == 3 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
|
||||
.GroupBy("Host");
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
namespace NzbDrone.Core.IndexerStats
|
||||
{
|
||||
public interface IIndexerStatisticsService
|
||||
{
|
||||
List<IndexerStatistics> IndexerStatistics();
|
||||
List<UserAgentStatistics> UserAgentStatistics();
|
||||
List<HostStatistics> HostStatistics();
|
||||
CombinedStatistics IndexerStatistics(DateTime start, DateTime end);
|
||||
}
|
||||
|
||||
public class IndexerStatisticsService : IIndexerStatisticsService
|
||||
{
|
||||
private readonly IIndexerStatisticsRepository _indexerStatisticsRepository;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly IHistoryService _historyService;
|
||||
|
||||
public IndexerStatisticsService(IIndexerStatisticsRepository indexerStatisticsRepository)
|
||||
public IndexerStatisticsService(IHistoryService historyService, IIndexerFactory indexerFactory)
|
||||
{
|
||||
_indexerStatisticsRepository = indexerStatisticsRepository;
|
||||
_historyService = historyService;
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public List<IndexerStatistics> IndexerStatistics()
|
||||
public CombinedStatistics IndexerStatistics(DateTime start, DateTime end)
|
||||
{
|
||||
var indexerStatistics = _indexerStatisticsRepository.IndexerStatistics();
|
||||
var history = _historyService.Between(start, end);
|
||||
|
||||
return indexerStatistics.ToList();
|
||||
}
|
||||
var groupedByIndexer = history.GroupBy(h => h.IndexerId);
|
||||
var groupedByUserAgent = history.GroupBy(h => h.Data.GetValueOrDefault("source") ?? "");
|
||||
var groupedByHost = history.GroupBy(h => h.Data.GetValueOrDefault("host") ?? "");
|
||||
|
||||
public List<UserAgentStatistics> UserAgentStatistics()
|
||||
{
|
||||
var userAgentStatistics = _indexerStatisticsRepository.UserAgentStatistics();
|
||||
var indexerStatsList = new List<IndexerStatistics>();
|
||||
var userAgentStatsList = new List<UserAgentStatistics>();
|
||||
var hostStatsList = new List<HostStatistics>();
|
||||
|
||||
return userAgentStatistics.ToList();
|
||||
}
|
||||
var indexers = _indexerFactory.All();
|
||||
|
||||
public List<HostStatistics> HostStatistics()
|
||||
{
|
||||
var hostStatistics = _indexerStatisticsRepository.HostStatistics();
|
||||
foreach (var indexer in groupedByIndexer)
|
||||
{
|
||||
var indexerDef = indexers.SingleOrDefault(i => i.Id == indexer.Key);
|
||||
|
||||
return hostStatistics.ToList();
|
||||
if (indexerDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var indexerStats = new IndexerStatistics
|
||||
{
|
||||
IndexerId = indexer.Key,
|
||||
IndexerName = indexerDef.Name
|
||||
};
|
||||
|
||||
var sortedEvents = indexer.OrderBy(v => v.Date)
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
indexerStats.AverageResponseTime = (int)sortedEvents.Where(h => h.Data.ContainsKey("elapsedTime")).Select(h => int.Parse(h.Data.GetValueOrDefault("elapsedTime"))).Average();
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
var failed = !historyEvent.Successful;
|
||||
switch (historyEvent.EventType)
|
||||
{
|
||||
case HistoryEventType.IndexerQuery:
|
||||
indexerStats.NumberOfQueries++;
|
||||
if (failed)
|
||||
{
|
||||
indexerStats.NumberOfFailedQueries++;
|
||||
}
|
||||
|
||||
break;
|
||||
case HistoryEventType.IndexerAuth:
|
||||
indexerStats.NumberOfAuthQueries++;
|
||||
if (failed)
|
||||
{
|
||||
indexerStats.NumberOfFailedAuthQueries++;
|
||||
}
|
||||
|
||||
break;
|
||||
case HistoryEventType.ReleaseGrabbed:
|
||||
indexerStats.NumberOfGrabs++;
|
||||
if (failed)
|
||||
{
|
||||
indexerStats.NumberOfFailedGrabs++;
|
||||
}
|
||||
|
||||
break;
|
||||
case HistoryEventType.IndexerRss:
|
||||
indexerStats.NumberOfRssQueries++;
|
||||
if (failed)
|
||||
{
|
||||
indexerStats.NumberOfFailedRssQueries++;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
indexerStatsList.Add(indexerStats);
|
||||
}
|
||||
|
||||
foreach (var indexer in groupedByUserAgent)
|
||||
{
|
||||
var indexerStats = new UserAgentStatistics
|
||||
{
|
||||
UserAgent = indexer.Key
|
||||
};
|
||||
|
||||
var sortedEvents = indexer.OrderBy(v => v.Date)
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
switch (historyEvent.EventType)
|
||||
{
|
||||
case HistoryEventType.IndexerRss:
|
||||
case HistoryEventType.IndexerQuery:
|
||||
indexerStats.NumberOfQueries++;
|
||||
|
||||
break;
|
||||
case HistoryEventType.ReleaseGrabbed:
|
||||
indexerStats.NumberOfGrabs++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
userAgentStatsList.Add(indexerStats);
|
||||
}
|
||||
|
||||
foreach (var indexer in groupedByHost)
|
||||
{
|
||||
var indexerStats = new HostStatistics
|
||||
{
|
||||
Host = indexer.Key
|
||||
};
|
||||
|
||||
var sortedEvents = indexer.OrderBy(v => v.Date)
|
||||
.ThenBy(v => v.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach (var historyEvent in sortedEvents)
|
||||
{
|
||||
switch (historyEvent.EventType)
|
||||
{
|
||||
case HistoryEventType.IndexerRss:
|
||||
case HistoryEventType.IndexerQuery:
|
||||
indexerStats.NumberOfQueries++;
|
||||
break;
|
||||
case HistoryEventType.ReleaseGrabbed:
|
||||
indexerStats.NumberOfGrabs++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hostStatsList.Add(indexerStats);
|
||||
}
|
||||
|
||||
return new CombinedStatistics
|
||||
{
|
||||
IndexerStatistics = indexerStatsList,
|
||||
UserAgentStatistics = userAgentStatsList,
|
||||
HostStatistics = hostStatsList
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
value = selection.TextContent;
|
||||
}
|
||||
|
||||
return ApplyFilters(ParseUtil.NormalizeSpace(value), selector.Filters, variables);
|
||||
return ApplyFilters(value.Trim(), selector.Filters, variables);
|
||||
}
|
||||
|
||||
protected string HandleJsonSelector(SelectorBlock selector, JToken parentObj, Dictionary<string, object> variables = null, bool required = true)
|
||||
@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
return ApplyFilters(ParseUtil.NormalizeSpace(value), selector.Filters, variables);
|
||||
return ApplyFilters(value.Trim(), selector.Filters, variables);
|
||||
}
|
||||
|
||||
protected Dictionary<string, object> GetBaseTemplateVariables()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
@@ -96,9 +95,12 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
|
||||
releaseInfo = base.ProcessItem(item, releaseInfo);
|
||||
releaseInfo.ImdbId = GetImdbId(item);
|
||||
releaseInfo.Grabs = GetGrabs(item);
|
||||
releaseInfo.Files = GetFiles(item);
|
||||
releaseInfo.ImdbId = GetIntAttribute(item, "imdb");
|
||||
releaseInfo.TmdbId = GetIntAttribute(item, "tmdb");
|
||||
releaseInfo.TvdbId = GetIntAttribute(item, "tvdbid");
|
||||
releaseInfo.TvRageId = GetIntAttribute(item, "rageid");
|
||||
releaseInfo.Grabs = GetIntAttribute(item, "grabs");
|
||||
releaseInfo.Files = GetIntAttribute(item, "files");
|
||||
releaseInfo.PosterUrl = GetPosterUrl(item);
|
||||
|
||||
return releaseInfo;
|
||||
@@ -195,27 +197,14 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return url;
|
||||
}
|
||||
|
||||
protected virtual int GetImdbId(XElement item)
|
||||
protected virtual int GetIntAttribute(XElement item, string attribute)
|
||||
{
|
||||
var imdbIdString = TryGetNewznabAttribute(item, "imdb");
|
||||
int imdbId;
|
||||
var idString = TryGetNewznabAttribute(item, attribute);
|
||||
int idInt;
|
||||
|
||||
if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out imdbId))
|
||||
if (!idString.IsNullOrWhiteSpace() && int.TryParse(idString, out idInt))
|
||||
{
|
||||
return imdbId;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual int GetGrabs(XElement item)
|
||||
{
|
||||
var grabsString = TryGetNewznabAttribute(item, "grabs");
|
||||
int grabs;
|
||||
|
||||
if (!grabsString.IsNullOrWhiteSpace() && int.TryParse(grabsString, out grabs))
|
||||
{
|
||||
return grabs;
|
||||
return idInt;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -226,19 +215,6 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
return ParseUrl(TryGetNewznabAttribute(item, "coverurl"));
|
||||
}
|
||||
|
||||
protected virtual int GetFiles(XElement item)
|
||||
{
|
||||
var filesString = TryGetNewznabAttribute(item, "files");
|
||||
int files;
|
||||
|
||||
if (!filesString.IsNullOrWhiteSpace() && int.TryParse(filesString, out files))
|
||||
{
|
||||
return files;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual int GetImdbYear(XElement item)
|
||||
{
|
||||
var imdbYearString = TryGetNewznabAttribute(item, "imdbyear");
|
||||
|
||||
459
src/NzbDrone.Core/Indexers/Definitions/PornoLab.cs
Normal file
459
src/NzbDrone.Core/Indexers/Definitions/PornoLab.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class PornoLab : TorrentIndexerBase<PornoLabSettings>
|
||||
{
|
||||
public override string Name => "PornoLab";
|
||||
public override string[] IndexerUrls => new string[] { "https://pornolab.net/" };
|
||||
private string LoginUrl => Settings.BaseUrl + "forum/login.php";
|
||||
public override string Description => "PornoLab is a Semi-Private Russian site for Adult content";
|
||||
public override string Language => "ru-ru";
|
||||
public override Encoding Encoding => Encoding.GetEncoding("windows-1251");
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public PornoLab(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new PornoLabRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new PornoLabParser(Settings, Capabilities.Categories, _logger);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true,
|
||||
Method = HttpMethod.POST
|
||||
};
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("login_username", Settings.Username)
|
||||
.AddFormParameter("login_password", Settings.Password)
|
||||
.AddFormParameter("login", "Login")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var errorMessage = "Unknown error message, please report";
|
||||
var loginResultParser = new HtmlParser();
|
||||
var loginResultDocument = loginResultParser.ParseDocument(response.Content);
|
||||
var errormsg = loginResultDocument.QuerySelector("h4[class=\"warnColor1 tCenter mrg_16\"]");
|
||||
if (errormsg != null)
|
||||
{
|
||||
errorMessage = errormsg.TextContent;
|
||||
}
|
||||
|
||||
throw new IndexerAuthException(errorMessage);
|
||||
}
|
||||
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("PornoLab authentication succeeded");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("Вы зашли как:"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(1768, NewznabStandardCategory.XXX, "Эротические фильмы / Erotic Movies");
|
||||
caps.Categories.AddCategoryMapping(60, NewznabStandardCategory.XXX, "Документальные фильмы / Documentary & Reality");
|
||||
caps.Categories.AddCategoryMapping(1644, NewznabStandardCategory.XXX, "Нудизм-Натуризм / Nudity");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1111, NewznabStandardCategory.XXXPack, "Паки полных фильмов / Full Length Movies Packs");
|
||||
caps.Categories.AddCategoryMapping(508, NewznabStandardCategory.XXX, "Классические фильмы / Classic");
|
||||
caps.Categories.AddCategoryMapping(555, NewznabStandardCategory.XXX, "Фильмы с сюжетом / Feature & Vignettes");
|
||||
caps.Categories.AddCategoryMapping(1673, NewznabStandardCategory.XXX, "Гонзо-фильмы 2011-2021 / Gonzo 2011-2021");
|
||||
caps.Categories.AddCategoryMapping(1112, NewznabStandardCategory.XXX, "Фильмы без сюжета 1991-2010 / All Sex & Amateur 1991-2010");
|
||||
caps.Categories.AddCategoryMapping(1718, NewznabStandardCategory.XXX, "Фильмы без сюжета 2011-2021 / All Sex & Amateur 2011-2021");
|
||||
caps.Categories.AddCategoryMapping(553, NewznabStandardCategory.XXX, "Лесбо-фильмы / All Girl & Solo");
|
||||
caps.Categories.AddCategoryMapping(1143, NewznabStandardCategory.XXX, "Этнические фильмы / Ethnic-Themed");
|
||||
caps.Categories.AddCategoryMapping(1646, NewznabStandardCategory.XXX, "Видео для телефонов и КПК / Pocket РС & Phone Video");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1712, NewznabStandardCategory.XXX, "Эротические и Документальные фильмы (DVD и HD) / EroticDocumentary & Reality (DVD & HD)");
|
||||
caps.Categories.AddCategoryMapping(1713, NewznabStandardCategory.XXXDVD, "Фильмы с сюжетомКлассические (DVD) / Feature & Vignettes, Classic (DVD)");
|
||||
caps.Categories.AddCategoryMapping(512, NewznabStandardCategory.XXXDVD, "ГонзоЛесбо и Фильмы без сюжета (DVD) / Gonzo, All Girl & Solo, All Sex (DVD)");
|
||||
caps.Categories.AddCategoryMapping(1775, NewznabStandardCategory.XXX, "Фильмы с сюжетом (HD Video) / Feature & Vignettes (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1450, NewznabStandardCategory.XXX, "ГонзоЛесбо и Фильмы без сюжета (HD Video) / Gonzo, All Girl & Solo, All Sex (HD Video)");
|
||||
|
||||
caps.Categories.AddCategoryMapping(902, NewznabStandardCategory.XXX, "Русские порнофильмы / Russian Full Length Movies");
|
||||
caps.Categories.AddCategoryMapping(1675, NewznabStandardCategory.XXXPack, "Паки русских порнороликов / Russian Clips Packs");
|
||||
caps.Categories.AddCategoryMapping(36, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 1991-2015 / Russian SiteRip's 1991-2015");
|
||||
caps.Categories.AddCategoryMapping(1830, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 1991-2015 (HD Video) / Russian SiteRip's 1991-2015 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1803, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2021 / Russian SiteRip's 2016-2021");
|
||||
caps.Categories.AddCategoryMapping(1831, NewznabStandardCategory.XXX, "Сайтрипы с русскими актрисами 2016-2021 (HD Video) / Russian SiteRip's 2016-2021 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1741, NewznabStandardCategory.XXX, "Русские Порноролики Разное / Russian Clips (various)");
|
||||
caps.Categories.AddCategoryMapping(1676, NewznabStandardCategory.XXX, "Русское любительское видео / Russian Amateur Video");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1780, NewznabStandardCategory.XXXPack, "Паки сайтрипов (HD Video) / SiteRip's Packs (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1110, NewznabStandardCategory.XXXPack, "Паки сайтрипов / SiteRip's Packs");
|
||||
caps.Categories.AddCategoryMapping(1678, NewznabStandardCategory.XXXPack, "Паки порнороликов по актрисам / Actresses Clips Packs");
|
||||
caps.Categories.AddCategoryMapping(1124, NewznabStandardCategory.XXX, "Сайтрипы 1991-2010 (HD Video) / SiteRip's 1991-2010 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1784, NewznabStandardCategory.XXX, "Сайтрипы 2011-2012 (HD Video) / SiteRip's 2011-2012 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1769, NewznabStandardCategory.XXX, "Сайтрипы 2013 (HD Video) / SiteRip's 2013 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1793, NewznabStandardCategory.XXX, "Сайтрипы 2014 (HD Video) / SiteRip's 2014 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1797, NewznabStandardCategory.XXX, "Сайтрипы 2015 (HD Video) / SiteRip's 2015 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1804, NewznabStandardCategory.XXX, "Сайтрипы 2016 (HD Video) / SiteRip's 2016 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1819, NewznabStandardCategory.XXX, "Сайтрипы 2017 (HD Video) / SiteRip's 2017 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1825, NewznabStandardCategory.XXX, "Сайтрипы 2018 (HD Video) / SiteRip's 2018 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1836, NewznabStandardCategory.XXX, "Сайтрипы 2019 (HD Video) / SiteRip's 2019 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1842, NewznabStandardCategory.XXX, "Сайтрипы 2020 (HD Video) / SiteRip's 2020 (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1846, NewznabStandardCategory.XXX, "Сайтрипы 2021 (HD Video) / SiteRip's 2021 (HD Video)");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1451, NewznabStandardCategory.XXX, "Сайтрипы 1991-2010 / SiteRip's 1991-2010");
|
||||
caps.Categories.AddCategoryMapping(1788, NewznabStandardCategory.XXX, "Сайтрипы 2011-2012 / SiteRip's 2011-2012");
|
||||
caps.Categories.AddCategoryMapping(1789, NewznabStandardCategory.XXX, "Сайтрипы 2013 / SiteRip's 2013");
|
||||
caps.Categories.AddCategoryMapping(1792, NewznabStandardCategory.XXX, "Сайтрипы 2014 / SiteRip's 2014");
|
||||
caps.Categories.AddCategoryMapping(1798, NewznabStandardCategory.XXX, "Сайтрипы 2015 / SiteRip's 2015");
|
||||
caps.Categories.AddCategoryMapping(1805, NewznabStandardCategory.XXX, "Сайтрипы 2016 / SiteRip's 2016");
|
||||
caps.Categories.AddCategoryMapping(1820, NewznabStandardCategory.XXX, "Сайтрипы 2017 / SiteRip's 2017");
|
||||
caps.Categories.AddCategoryMapping(1826, NewznabStandardCategory.XXX, "Сайтрипы 2018 / SiteRip's 2018");
|
||||
caps.Categories.AddCategoryMapping(1837, NewznabStandardCategory.XXX, "Сайтрипы 2019 / SiteRip's 2019");
|
||||
caps.Categories.AddCategoryMapping(1843, NewznabStandardCategory.XXX, "Сайтрипы 2020 / SiteRip's 2020");
|
||||
caps.Categories.AddCategoryMapping(1847, NewznabStandardCategory.XXX, "Сайтрипы 2021 / SiteRip's 2021");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1707, NewznabStandardCategory.XXX, "Сцены из фильмов / Movie Scenes");
|
||||
caps.Categories.AddCategoryMapping(284, NewznabStandardCategory.XXX, "Порноролики Разное / Clips (various)");
|
||||
caps.Categories.AddCategoryMapping(1823, NewznabStandardCategory.XXX, "Порноролики в 3D и Virtual Reality (VR) / 3D & Virtual Reality Videos");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1801, NewznabStandardCategory.XXXPack, "Паки японских фильмов и сайтрипов / Full Length Japanese Movies Packs & SiteRip's Packs");
|
||||
caps.Categories.AddCategoryMapping(1719, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы (DVD и HD Video) / Japanese Movies & SiteRip's (DVD & HD Video)");
|
||||
caps.Categories.AddCategoryMapping(997, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы 1991-2014 / Japanese Movies & SiteRip's 1991-2014");
|
||||
caps.Categories.AddCategoryMapping(1818, NewznabStandardCategory.XXX, "Японские фильмы и сайтрипы 2015-2019 / Japanese Movies & SiteRip's 2015-2019");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1671, NewznabStandardCategory.XXX, "Эротические студии (видео) / Erotic Video Library");
|
||||
caps.Categories.AddCategoryMapping(1726, NewznabStandardCategory.XXX, "Met-Art & MetModels");
|
||||
caps.Categories.AddCategoryMapping(883, NewznabStandardCategory.XXXImageSet, "Эротические студии Разное / Erotic Picture Gallery (various)");
|
||||
caps.Categories.AddCategoryMapping(1759, NewznabStandardCategory.XXXImageSet, "Паки сайтрипов эротических студий / Erotic Picture SiteRip's Packs");
|
||||
caps.Categories.AddCategoryMapping(1728, NewznabStandardCategory.XXXImageSet, "Любительское фото / Amateur Picture Gallery");
|
||||
caps.Categories.AddCategoryMapping(1729, NewznabStandardCategory.XXXPack, "Подборки по актрисам / Actresses Picture Packs");
|
||||
caps.Categories.AddCategoryMapping(38, NewznabStandardCategory.XXXImageSet, "Подборки сайтрипов / SiteRip's Picture Packs");
|
||||
caps.Categories.AddCategoryMapping(1757, NewznabStandardCategory.XXXImageSet, "Подборки сетов / Picture Sets Packs");
|
||||
caps.Categories.AddCategoryMapping(1735, NewznabStandardCategory.XXXImageSet, "Тематическое и нетрадиционное фото / Misc & Special Interest Picture Packs");
|
||||
caps.Categories.AddCategoryMapping(1731, NewznabStandardCategory.XXXImageSet, "Журналы / Magazines");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1679, NewznabStandardCategory.XXX, "Хентай: основной подраздел / Hentai: main subsection");
|
||||
caps.Categories.AddCategoryMapping(1740, NewznabStandardCategory.XXX, "Хентай в высоком качестве (DVD и HD) / Hentai DVD & HD");
|
||||
caps.Categories.AddCategoryMapping(1834, NewznabStandardCategory.XXX, "Хентай: ролики 2D / Hentai: 2D video");
|
||||
caps.Categories.AddCategoryMapping(1752, NewznabStandardCategory.XXX, "Хентай: ролики 3D / Hentai: 3D video");
|
||||
caps.Categories.AddCategoryMapping(1760, NewznabStandardCategory.XXX, "Хентай: Манга / Hentai: Manga");
|
||||
caps.Categories.AddCategoryMapping(1781, NewznabStandardCategory.XXX, "Хентай: Арт и HCG / Hentai: Artwork & HCG");
|
||||
caps.Categories.AddCategoryMapping(1711, NewznabStandardCategory.XXX, "Мультфильмы / Cartoons");
|
||||
caps.Categories.AddCategoryMapping(1296, NewznabStandardCategory.XXX, "Комиксы и рисунки / Comics & Artwork");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1750, NewznabStandardCategory.XXX, "Игры: основной подраздел / Games: main subsection");
|
||||
caps.Categories.AddCategoryMapping(1756, NewznabStandardCategory.XXX, "Игры: визуальные новеллы / Games: Visual Novels");
|
||||
caps.Categories.AddCategoryMapping(1785, NewznabStandardCategory.XXX, "Игры: ролевые / Games: role-playing (RPG Maker and WOLF RPG Editor)");
|
||||
caps.Categories.AddCategoryMapping(1790, NewznabStandardCategory.XXX, "Игры и Софт: Анимация / Software: Animation");
|
||||
caps.Categories.AddCategoryMapping(1827, NewznabStandardCategory.XXX, "Игры: В разработке и Демо (основной подраздел) / Games: In Progress and Demo (main subsection)");
|
||||
caps.Categories.AddCategoryMapping(1828, NewznabStandardCategory.XXX, "Игры: В разработке и Демо (ролевые) / Games: In Progress and Demo (role-playing - RPG Maker and WOLF RPG Editor)");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1715, NewznabStandardCategory.XXX, "Транссексуалы (DVD и HD) / Transsexual (DVD & HD)");
|
||||
caps.Categories.AddCategoryMapping(1680, NewznabStandardCategory.XXX, "Транссексуалы / Transsexual");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1758, NewznabStandardCategory.XXX, "Бисексуалы / Bisexual");
|
||||
caps.Categories.AddCategoryMapping(1682, NewznabStandardCategory.XXX, "БДСМ / BDSM");
|
||||
caps.Categories.AddCategoryMapping(1733, NewznabStandardCategory.XXX, "Женское доминирование и страпон / Femdom & Strapon");
|
||||
caps.Categories.AddCategoryMapping(1754, NewznabStandardCategory.XXX, "Подглядывание / Voyeur");
|
||||
caps.Categories.AddCategoryMapping(1734, NewznabStandardCategory.XXX, "Фистинг и дилдо / Fisting & Dildo");
|
||||
caps.Categories.AddCategoryMapping(1791, NewznabStandardCategory.XXX, "Беременные / Pregnant");
|
||||
caps.Categories.AddCategoryMapping(509, NewznabStandardCategory.XXX, "Буккаке / Bukkake");
|
||||
caps.Categories.AddCategoryMapping(1685, NewznabStandardCategory.XXX, "Мочеиспускание / Peeing");
|
||||
caps.Categories.AddCategoryMapping(1762, NewznabStandardCategory.XXX, "Фетиш / Fetish");
|
||||
|
||||
caps.Categories.AddCategoryMapping(903, NewznabStandardCategory.XXX, "Полнометражные гей-фильмы / Full Length Movies (Gay)");
|
||||
caps.Categories.AddCategoryMapping(1765, NewznabStandardCategory.XXX, "Полнометражные азиатские гей-фильмы / Full-length Asian Films (Gay)");
|
||||
caps.Categories.AddCategoryMapping(1767, NewznabStandardCategory.XXX, "Классические гей-фильмы (до 1990 года) / Classic Gay Films (Pre-1990's)");
|
||||
caps.Categories.AddCategoryMapping(1755, NewznabStandardCategory.XXX, "Гей-фильмы в высоком качестве (DVD и HD) / High-Quality Full Length Movies (Gay DVD & HD)");
|
||||
caps.Categories.AddCategoryMapping(1787, NewznabStandardCategory.XXX, "Азиатские гей-фильмы в высоком качестве (DVD и HD) / High-Quality Full Length Asian Movies (Gay DVD & HD)");
|
||||
caps.Categories.AddCategoryMapping(1763, NewznabStandardCategory.XXXPack, "ПАКи гей-роликов и сайтрипов / Clip's & SiteRip's Packs (Gay)");
|
||||
caps.Categories.AddCategoryMapping(1777, NewznabStandardCategory.XXX, "Гей-ролики в высоком качестве (HD Video) / Gay Clips (HD Video)");
|
||||
caps.Categories.AddCategoryMapping(1691, NewznabStandardCategory.XXX, "РоликиSiteRip'ы и сцены из гей-фильмов / Clips & Movie Scenes (Gay)");
|
||||
caps.Categories.AddCategoryMapping(1692, NewznabStandardCategory.XXXImageSet, "Гей-журналыфото, разное / Magazines, Photo, Rest (Gay)");
|
||||
|
||||
caps.Categories.AddCategoryMapping(1817, NewznabStandardCategory.XXX, "Обход блокировки");
|
||||
caps.Categories.AddCategoryMapping(1670, NewznabStandardCategory.XXX, "Эротическое видео / Erotic&Softcore");
|
||||
caps.Categories.AddCategoryMapping(1672, NewznabStandardCategory.XXX, "Зарубежные порнофильмы / Full Length Movies");
|
||||
caps.Categories.AddCategoryMapping(1717, NewznabStandardCategory.XXX, "Зарубежные фильмы в высоком качестве (DVD&HD) / Full Length ..");
|
||||
caps.Categories.AddCategoryMapping(1674, NewznabStandardCategory.XXX, "Русское порно / Russian Video");
|
||||
caps.Categories.AddCategoryMapping(1677, NewznabStandardCategory.XXX, "Зарубежные порноролики / Clips");
|
||||
caps.Categories.AddCategoryMapping(1800, NewznabStandardCategory.XXX, "Японское порно / Japanese Adult Video (JAV)");
|
||||
caps.Categories.AddCategoryMapping(1815, NewznabStandardCategory.XXX, "Архив (Японское порно)");
|
||||
caps.Categories.AddCategoryMapping(1723, NewznabStandardCategory.XXX, "Эротические студиифото и журналы / Erotic Picture Gallery ..");
|
||||
caps.Categories.AddCategoryMapping(1802, NewznabStandardCategory.XXX, "Архив (Фото)");
|
||||
caps.Categories.AddCategoryMapping(1745, NewznabStandardCategory.XXX, "Хентай и МангаМультфильмы и Комиксы, Рисунки / Hentai&Ma..");
|
||||
caps.Categories.AddCategoryMapping(1838, NewznabStandardCategory.XXX, "Игры / Games");
|
||||
caps.Categories.AddCategoryMapping(1829, NewznabStandardCategory.XXX, "Обсуждение игр / Games Discussion");
|
||||
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.XXX, "Нетрадиционное порно / Special Interest Movies&Clips");
|
||||
caps.Categories.AddCategoryMapping(1681, NewznabStandardCategory.XXX, "Дефекация / Scat");
|
||||
caps.Categories.AddCategoryMapping(1683, NewznabStandardCategory.XXX, "Архив (общий)");
|
||||
caps.Categories.AddCategoryMapping(1688, NewznabStandardCategory.XXX, "Гей-порно / Gay Forum");
|
||||
caps.Categories.AddCategoryMapping(1720, NewznabStandardCategory.XXX, "Архив (Гей-порно)");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class PornoLabRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public PornoLabSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public PornoLabRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/forum/tracker.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
var searchString = term;
|
||||
|
||||
// NameValueCollection don't support cat[]=19&cat[]=6
|
||||
var qc = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
{ "o", "1" },
|
||||
{ "s", "2" }
|
||||
};
|
||||
|
||||
// if the search string is empty use the getnew view
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
qc.Add("nm", searchString);
|
||||
}
|
||||
else
|
||||
{
|
||||
// use the normal search
|
||||
searchString = searchString.Replace("-", " ");
|
||||
qc.Add("nm", searchString);
|
||||
}
|
||||
|
||||
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
|
||||
{
|
||||
qc.Add("f[]", cat);
|
||||
}
|
||||
|
||||
searchUrl = searchUrl + "?" + qc.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class PornoLabParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly PornoLabSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly Logger _logger;
|
||||
private static readonly Regex StripRussianRegex = new Regex(@"(\([А-Яа-яЁё\W]+\))|(^[А-Яа-яЁё\W\d]+\/ )|([а-яА-ЯЁё \-]+,+)|([а-яА-ЯЁё]+)");
|
||||
|
||||
public PornoLabParser(PornoLabSettings settings, IndexerCapabilitiesCategories categories, Logger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var rowsSelector = "table#tor-tbl > tbody > tr";
|
||||
|
||||
var searchResultParser = new HtmlParser();
|
||||
var searchResultDocument = searchResultParser.ParseDocument(indexerResponse.Content);
|
||||
var rows = searchResultDocument.QuerySelectorAll(rowsSelector);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
try
|
||||
{
|
||||
var qDownloadLink = row.QuerySelector("a.tr-dl");
|
||||
|
||||
// Expects moderation
|
||||
if (qDownloadLink == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var qForumLink = row.QuerySelector("a.f");
|
||||
var qDetailsLink = row.QuerySelector("a.tLink");
|
||||
var qSize = row.QuerySelector("td:nth-child(6) u");
|
||||
var link = new Uri(_settings.BaseUrl + "forum/" + qDetailsLink.GetAttribute("href"));
|
||||
var seederString = row.QuerySelector("td:nth-child(7) b").TextContent;
|
||||
var seeders = string.IsNullOrWhiteSpace(seederString) ? 0 : ParseUtil.CoerceInt(seederString);
|
||||
|
||||
var timestr = row.QuerySelector("td:nth-child(11) u").TextContent;
|
||||
var forum = qForumLink;
|
||||
var forumid = forum.GetAttribute("href").Split('=')[1];
|
||||
var title = _settings.StripRussianLetters
|
||||
? StripRussianRegex.Replace(qDetailsLink.TextContent, "")
|
||||
: qDetailsLink.TextContent;
|
||||
var size = ParseUtil.GetBytes(qSize.TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent);
|
||||
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(9)").TextContent);
|
||||
var publishDate = DateTimeUtil.UnixTimestampToDateTime(long.Parse(timestr));
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 0,
|
||||
Title = title,
|
||||
InfoUrl = link.AbsoluteUri,
|
||||
Description = qForumLink.TextContent,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
Guid = link.AbsoluteUri,
|
||||
Size = size,
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
Grabs = grabs,
|
||||
PublishDate = publishDate,
|
||||
Categories = _categories.MapTrackerCatToNewznab(forumid),
|
||||
DownloadVolumeFactor = 1,
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(string.Format("Pornolab: Error while parsing row '{0}':\n\n{1}", row.OuterHtml, ex));
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class PornoLabSettingsValidator : AbstractValidator<PornoLabSettings>
|
||||
{
|
||||
public PornoLabSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class PornoLabSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly PornoLabSettingsValidator Validator = new PornoLabSettingsValidator();
|
||||
|
||||
public PornoLabSettings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Base Url", HelpText = "Select which baseurl Prowlarr will use for requests to the site", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Strip Russian Letters", HelpLink = "Strip Cyrillic letters from release names", Type = FieldType.Checkbox)]
|
||||
public bool StripRussianLetters { get; set; }
|
||||
|
||||
[FieldDefinition(5)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +244,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
UploadVolumeFactor = 1,
|
||||
Guid = details,
|
||||
InfoUrl = details,
|
||||
DownloadUrl = _settings.BaseUrl + "download.php?id=" + id,
|
||||
DownloadUrl = _settings.BaseUrl + "download.php?id=" + id + "&apikey=" + _settings.ApiKey,
|
||||
Title = row.Value<string>("name"),
|
||||
Categories = _categories.MapTrackerCatToNewznab(row.Value<int>("category").ToString()),
|
||||
PublishDate = dateTime.AddSeconds(row.Value<long>("added")).ToLocalTime(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Xthor
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_torrentDetailsUrl = _settings.BaseUrl + "details.php?id={id}";
|
||||
_torrentDetailsUrl = _settings.BaseUrl.Replace("api.", "") + "details.php?id={id}";
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Data;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using NLog;
|
||||
using NLog.Common;
|
||||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
@@ -13,7 +15,7 @@ namespace NzbDrone.Core.Instrumentation
|
||||
{
|
||||
public class DatabaseTarget : TargetWithLayout, IHandle<ApplicationShutdownRequested>
|
||||
{
|
||||
private const string INSERT_COMMAND = "INSERT INTO [Logs]([Message],[Time],[Logger],[Exception],[ExceptionType],[Level]) " +
|
||||
private const string INSERT_COMMAND = "INSERT INTO \"Logs\" (\"Message\",\"Time\",\"Logger\",\"Exception\",\"ExceptionType\",\"Level\") " +
|
||||
"VALUES(@Message,@Time,@Logger,@Exception,@ExceptionType,@Level)";
|
||||
|
||||
private readonly IConnectionStringFactory _connectionStringFactory;
|
||||
@@ -83,23 +85,16 @@ namespace NzbDrone.Core.Instrumentation
|
||||
|
||||
log.Level = logEvent.Level.Name;
|
||||
|
||||
using (var connection =
|
||||
SQLiteFactory.Instance.CreateConnection())
|
||||
{
|
||||
connection.ConnectionString = _connectionStringFactory.LogDbConnectionString;
|
||||
connection.Open();
|
||||
using (var sqlCommand = connection.CreateCommand())
|
||||
{
|
||||
sqlCommand.CommandText = INSERT_COMMAND;
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level });
|
||||
var connectionString = _connectionStringFactory.LogDbConnectionString;
|
||||
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
}
|
||||
//TODO: Probably need more robust way to differentiate what's being used
|
||||
if (connectionString.Contains(".db"))
|
||||
{
|
||||
WriteSqliteLog(log, connectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
WritePostgresLog(log, connectionString);
|
||||
}
|
||||
}
|
||||
catch (SQLiteException ex)
|
||||
@@ -109,6 +104,48 @@ namespace NzbDrone.Core.Instrumentation
|
||||
}
|
||||
}
|
||||
|
||||
private void WritePostgresLog(Log log, string connectionString)
|
||||
{
|
||||
using (var connection =
|
||||
new NpgsqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (var sqlCommand = connection.CreateCommand())
|
||||
{
|
||||
sqlCommand.CommandText = INSERT_COMMAND;
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Exception", DbType.String) { Value = log.Exception == null ? DBNull.Value : log.Exception });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("ExceptionType", DbType.String) { Value = log.ExceptionType == null ? DBNull.Value : log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Level", DbType.String) { Value = log.Level });
|
||||
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSqliteLog(Log log, string connectionString)
|
||||
{
|
||||
using (var connection =
|
||||
SQLiteFactory.Instance.CreateConnection())
|
||||
{
|
||||
connection.ConnectionString = connectionString;
|
||||
connection.Open();
|
||||
using (var sqlCommand = connection.CreateCommand())
|
||||
{
|
||||
sqlCommand.CommandText = INSERT_COMMAND;
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level });
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ApplicationShutdownRequested message)
|
||||
{
|
||||
if (LogManager.Configuration?.LoggingRules?.Contains(Rule) == true)
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||
|
||||
public void OrphanStarted()
|
||||
{
|
||||
var sql = @"UPDATE Commands SET Status = @Orphaned, EndedAt = @Ended WHERE Status = @Started";
|
||||
var sql = @"UPDATE ""Commands"" SET ""Status"" = @Orphaned, ""EndedAt"" = @Ended WHERE ""Status"" = @Started";
|
||||
var args = new
|
||||
{
|
||||
Orphaned = (int)CommandStatus.Orphaned,
|
||||
|
||||
@@ -114,14 +114,14 @@ namespace NzbDrone.Core.Parser
|
||||
return dateTimeParsed;
|
||||
}
|
||||
|
||||
throw new Exception("FromFuzzyTime parsing failed");
|
||||
throw new Exception($"FromFuzzyTime parsing failed for string {str}");
|
||||
}
|
||||
|
||||
public static DateTime FromUnknown(string str, string format = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
str = ParseUtil.NormalizeSpace(str);
|
||||
str = str.Trim();
|
||||
if (str.ToLower().Contains("now"))
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
@@ -254,7 +254,7 @@ namespace NzbDrone.Core.Parser
|
||||
// converts a date/time string to a DateTime object using a GoLang layout
|
||||
public static DateTime ParseDateTimeGoLang(string date, string layout)
|
||||
{
|
||||
date = ParseUtil.NormalizeSpace(date);
|
||||
date = date.Trim();
|
||||
var pattern = layout;
|
||||
|
||||
// year
|
||||
|
||||
@@ -13,18 +13,16 @@ namespace NzbDrone.Core.Parser
|
||||
RegexOptions.Compiled);
|
||||
private static readonly Regex ImdbId = new Regex(@"^(?:tt)?(\d{1,8})$", RegexOptions.Compiled);
|
||||
|
||||
public static string NormalizeSpace(string s) => s.Trim();
|
||||
|
||||
public static string NormalizeMultiSpaces(string s) =>
|
||||
new Regex(@"\s+").Replace(NormalizeSpace(s), " ");
|
||||
new Regex(@"\s+").Replace(s.Trim(), " ");
|
||||
|
||||
public static string NormalizeNumber(string s)
|
||||
private static string NormalizeNumber(string s)
|
||||
{
|
||||
var valStr = new string(s.Where(c => char.IsDigit(c) || c == '.' || c == ',').ToArray());
|
||||
|
||||
valStr = (valStr.Length == 0) ? "0" : valStr.Replace(",", ".");
|
||||
|
||||
valStr = NormalizeSpace(valStr).Replace("-", "0");
|
||||
valStr = valStr.Trim().Replace("-", "0");
|
||||
|
||||
if (valStr.Count(c => c == '.') > 1)
|
||||
{
|
||||
@@ -120,7 +118,7 @@ namespace NzbDrone.Core.Parser
|
||||
return GetBytes(unit, val);
|
||||
}
|
||||
|
||||
public static long GetBytes(string unit, float value)
|
||||
private static long GetBytes(string unit, float value)
|
||||
{
|
||||
unit = unit.Replace("i", "").ToLowerInvariant();
|
||||
if (unit.Contains("kb"))
|
||||
@@ -146,12 +144,12 @@ namespace NzbDrone.Core.Parser
|
||||
return (long)value;
|
||||
}
|
||||
|
||||
public static long BytesFromTB(float tb) => BytesFromGB(tb * 1024f);
|
||||
private static long BytesFromTB(float tb) => BytesFromGB(tb * 1024f);
|
||||
|
||||
public static long BytesFromGB(float gb) => BytesFromMB(gb * 1024f);
|
||||
private static long BytesFromGB(float gb) => BytesFromMB(gb * 1024f);
|
||||
|
||||
public static long BytesFromMB(float mb) => BytesFromKB(mb * 1024f);
|
||||
private static long BytesFromMB(float mb) => BytesFromKB(mb * 1024f);
|
||||
|
||||
public static long BytesFromKB(float kb) => (long)(kb * 1024f);
|
||||
private static long BytesFromKB(float kb) => (long)(kb * 1024f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
<PackageReference Include="MailKit" Version="2.14.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.2" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
<PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" />
|
||||
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.1" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.1" />
|
||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="TinyTwitter" Version="1.1.2" />
|
||||
<PackageReference Include="Kveer.XmlRPC" Version="1.1.1" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-12" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.0" />
|
||||
<PackageReference Include="MonoTorrent" Version="1.0.29" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
|
||||
@@ -4,6 +4,7 @@ using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common;
|
||||
@@ -33,16 +34,15 @@ namespace NzbDrone.App.Test
|
||||
{
|
||||
var args = new StartupContext("first", "second");
|
||||
|
||||
// set up a dummy broadcaster to allow tests to resolve
|
||||
var mockBroadcaster = new Mock<IBroadcastSignalRMessage>();
|
||||
|
||||
var container = new Container(rules => rules.WithNzbDroneRules())
|
||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||
.AddNzbDroneLogger()
|
||||
.AddDummyDatabase()
|
||||
.AddStartupContext(args);
|
||||
|
||||
container.RegisterInstance<IBroadcastSignalRMessage>(mockBroadcaster.Object);
|
||||
// dummy lifetime and broadcaster so tests resolve
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object);
|
||||
|
||||
_container = container.GetServiceProvider();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace NzbDrone.Host
|
||||
private readonly IBrowserService _browserService;
|
||||
private readonly IProcessProvider _processProvider;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IUtilityModeRouter _utilityModeRouter;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AppLifetime(IHostApplicationLifetime appLifetime,
|
||||
@@ -29,7 +28,6 @@ namespace NzbDrone.Host
|
||||
IBrowserService browserService,
|
||||
IProcessProvider processProvider,
|
||||
IEventAggregator eventAggregator,
|
||||
IUtilityModeRouter utilityModeRouter,
|
||||
Logger logger)
|
||||
{
|
||||
_appLifetime = appLifetime;
|
||||
@@ -39,7 +37,6 @@ namespace NzbDrone.Host
|
||||
_browserService = browserService;
|
||||
_processProvider = processProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_utilityModeRouter = utilityModeRouter;
|
||||
_logger = logger;
|
||||
|
||||
appLifetime.ApplicationStarted.Register(OnAppStarted);
|
||||
@@ -71,7 +68,7 @@ namespace NzbDrone.Host
|
||||
|
||||
private void OnAppStopped()
|
||||
{
|
||||
if (_runtimeInfo.RestartPending)
|
||||
if (_runtimeInfo.RestartPending && !_runtimeInfo.IsWindowsService)
|
||||
{
|
||||
var restartArgs = GetRestartArgs();
|
||||
|
||||
|
||||
@@ -171,7 +171,20 @@ namespace NzbDrone.Host
|
||||
return ApplicationModes.UninstallService;
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && WindowsServiceHelpers.IsWindowsService())
|
||||
Logger.Debug("Getting windows service status");
|
||||
|
||||
// IsWindowsService can throw sometimes, so wrap it
|
||||
var isWindowsService = false;
|
||||
try
|
||||
{
|
||||
isWindowsService = WindowsServiceHelpers.IsWindowsService();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Failed to get service status");
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && isWindowsService)
|
||||
{
|
||||
return ApplicationModes.Service;
|
||||
}
|
||||
|
||||
@@ -16,21 +16,18 @@ namespace NzbDrone.Host
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConsoleService _consoleService;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IProcessProvider _processProvider;
|
||||
private readonly IRemoteAccessAdapter _remoteAccessAdapter;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UtilityModeRouter(IServiceProvider serviceProvider,
|
||||
IConsoleService consoleService,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
IProcessProvider processProvider,
|
||||
IRemoteAccessAdapter remoteAccessAdapter,
|
||||
Logger logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_consoleService = consoleService;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_processProvider = processProvider;
|
||||
_remoteAccessAdapter = remoteAccessAdapter;
|
||||
_logger = logger;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>..\NzbDrone.Host\Prowlarr.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,9 +4,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Processes;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Host;
|
||||
|
||||
namespace NzbDrone.SysTray
|
||||
@@ -14,28 +13,19 @@ namespace NzbDrone.SysTray
|
||||
public class SystemTrayApp : Form, IHostedService
|
||||
{
|
||||
private readonly IBrowserService _browserService;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IProcessProvider _processProvider;
|
||||
private readonly ILifecycleService _lifecycle;
|
||||
|
||||
private readonly NotifyIcon _trayIcon = new NotifyIcon();
|
||||
private readonly ContextMenuStrip _trayMenu = new ContextMenuStrip();
|
||||
|
||||
public SystemTrayApp(IBrowserService browserService, IRuntimeInfo runtimeInfo, IProcessProvider processProvider)
|
||||
public SystemTrayApp(IBrowserService browserService, ILifecycleService lifecycle)
|
||||
{
|
||||
_browserService = browserService;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_processProvider = processProvider;
|
||||
_lifecycle = lifecycle;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Application.ThreadException += OnThreadException;
|
||||
Application.ApplicationExit += OnApplicationExit;
|
||||
|
||||
Application.SetHighDpiMode(HighDpiMode.PerMonitor);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
_trayMenu.Items.Add(new ToolStripMenuItem("Launch Browser", null, LaunchBrowser));
|
||||
_trayMenu.Items.Add(new ToolStripSeparator());
|
||||
_trayMenu.Items.Add(new ToolStripMenuItem("Exit", null, OnExit));
|
||||
@@ -69,12 +59,6 @@ namespace NzbDrone.SysTray
|
||||
DisposeTrayIcon();
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
Console.WriteLine("Closing");
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
Visible = false;
|
||||
@@ -102,8 +86,7 @@ namespace NzbDrone.SysTray
|
||||
|
||||
private void OnExit(object sender, EventArgs e)
|
||||
{
|
||||
LogManager.Configuration = null;
|
||||
Environment.Exit(0);
|
||||
_lifecycle.Shutdown();
|
||||
}
|
||||
|
||||
private void LaunchBrowser(object sender, EventArgs e)
|
||||
@@ -117,33 +100,17 @@ namespace NzbDrone.SysTray
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplicationExit(object sender, EventArgs e)
|
||||
{
|
||||
if (_runtimeInfo.RestartPending)
|
||||
{
|
||||
_processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, "--restart --nobrowser");
|
||||
}
|
||||
|
||||
DisposeTrayIcon();
|
||||
}
|
||||
|
||||
private void OnThreadException(object sender, EventArgs e)
|
||||
{
|
||||
DisposeTrayIcon();
|
||||
}
|
||||
|
||||
private void DisposeTrayIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Icon = null;
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
if (_trayIcon == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Icon = null;
|
||||
_trayIcon.Visible = false;
|
||||
_trayIcon.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,22 @@ namespace NzbDrone
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
|
||||
try
|
||||
{
|
||||
var startupArgs = new StartupContext(args);
|
||||
|
||||
NzbDroneLogger.Register(startupArgs, false, true);
|
||||
|
||||
Bootstrap.Start(args, e =>
|
||||
{
|
||||
e.ConfigureServices((_, s) => s.AddSingleton<IHostedService, SystemTrayApp>());
|
||||
});
|
||||
Bootstrap.Start(args, e => { e.ConfigureServices((_, s) => s.AddSingleton<IHostedService, SystemTrayApp>()); });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal(e, "EPIC FAIL: " + e.Message);
|
||||
var message = string.Format("{0}: {1}", e.GetType().Name, e.Message);
|
||||
var message = string.Format("{0}: {1}", e.GetType().Name, e.ToString());
|
||||
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.net>
|
||||
<connectionManagement>
|
||||
<add address="*" maxconnection="100" />
|
||||
</connectionManagement>
|
||||
</system.net>
|
||||
<startup useLegacyV2RuntimeActivationPolicy="true">
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<loadFromRemoteSources enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -76,7 +76,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
_indexerService.DeleteIndexers(resource.IndexerIds);
|
||||
|
||||
return new object();
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.IndexerStats;
|
||||
using Prowlarr.Http;
|
||||
@@ -15,13 +16,16 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IndexerStatsResource GetAll()
|
||||
public IndexerStatsResource GetAll(DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
var statsStartDate = startDate ?? DateTime.Now.AddDays(-30);
|
||||
var statsEndDate = endDate ?? DateTime.Now;
|
||||
|
||||
var indexerResource = new IndexerStatsResource
|
||||
{
|
||||
Indexers = _indexerStatisticsService.IndexerStatistics(),
|
||||
UserAgents = _indexerStatisticsService.UserAgentStatistics(),
|
||||
Hosts = _indexerStatisticsService.HostStatistics()
|
||||
Indexers = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).IndexerStatistics,
|
||||
UserAgents = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).UserAgentStatistics,
|
||||
Hosts = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).HostStatistics
|
||||
};
|
||||
|
||||
return indexerResource;
|
||||
|
||||
@@ -102,9 +102,11 @@ namespace Prowlarr.Api.V1
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteProvider(int id)
|
||||
public object DeleteProvider(int id)
|
||||
{
|
||||
_providerFactory.Delete(id);
|
||||
|
||||
return new { };
|
||||
}
|
||||
|
||||
[HttpGet("schema")]
|
||||
|
||||
@@ -77,7 +77,8 @@ namespace Prowlarr.Api.V1.System
|
||||
Mode = _runtimeInfo.Mode,
|
||||
Branch = _configFileProvider.Branch,
|
||||
Authentication = _configFileProvider.AuthenticationMethod,
|
||||
SqliteVersion = _database.Version,
|
||||
DatabaseType = _database.DatabaseType,
|
||||
DatabaseVersion = _database.Version,
|
||||
MigrationVersion = _database.Migration,
|
||||
UrlBase = _configFileProvider.UrlBase,
|
||||
RuntimeVersion = _platformInfo.Version,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29418.71
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31912.275
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}"
|
||||
EndProject
|
||||
|
||||
Reference in New Issue
Block a user