Compare commits

...

12 Commits

Author SHA1 Message Date
Qstick
c83c818380 Fixed: (Flaresolverr) Ignore http errors on initial request when using FS 2021-12-01 21:50:28 -06:00
Qstick
a2df38b1ca Fixed: Windows installer and adding/removing services
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2021-12-01 21:46:33 -06:00
Qstick
89510c4a65 Fixed: Workaround net6 object serialization issues
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2021-12-01 20:39:26 -06:00
Qstick
b5a2f68bde Fixed: (Newznab) Parse Imdb, Tmdb, RageId, Tvdbid from indexer
Fixes #656
2021-11-30 21:47:44 -06:00
Qstick
1ffab661da Don't fallback to different OS Yarn cache 2021-11-30 11:20:15 -06:00
Qstick
bf0a627a4e New: (Indexer) PornoLab 2021-11-30 07:56:47 -06:00
Qstick
df764ce8b4 New: Postgres Support 2021-11-29 23:14:48 -06:00
Qstick
a61d4ab88c New: Stats filters 2021-11-29 23:14:48 -06:00
Qstick
01e7e924c4 Fixed: (Flaresolverr) Proxy Test
Fixes #651
2021-11-29 21:00:38 -06:00
Qstick
5f5df99dab Improved logging on bad date parse 2021-11-29 21:00:11 -06:00
bakerboy448
77e40e8e53 Fixed: (TorrentSyndikat) Download URL missing API 2021-11-29 19:36:47 -06:00
Qstick
d3853c1a54 Bump version to 0.1.6 2021-11-28 22:15:09 -06:00
50 changed files with 1206 additions and 291 deletions

View File

@@ -7,7 +7,7 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
majorVersion: '0.1.5'
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

View File

@@ -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
};

View File

@@ -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);

View 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;

View 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);

View File

@@ -14,7 +14,6 @@ function createRemoveItemHandler(section, url) {
const ajaxOptions = {
url: `${url}/${id}?${$.param(queryParams, true)}`,
dataType: 'text',
method: 'DELETE'
};

View File

@@ -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());
}
});
//

View File

@@ -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,

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Common.EnvironmentInfo
private readonly Logger _logger;
private readonly DateTime _startTime = DateTime.UtcNow;
public RuntimeInfo(IHostLifetime hostLifetime, Logger logger)
public RuntimeInfo(Logger logger, IHostLifetime hostLifetime = null)
{
_logger = logger;

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
});

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();

View File

@@ -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';");
}
}
}

View File

@@ -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' );");
}
}
}

View File

@@ -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);

View File

@@ -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" });
}
}
}

View File

@@ -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())

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)");
}
}

View File

@@ -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)");
}
}
}

View File

@@ -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)");
}
}
}

View File

@@ -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)");
}
}
}

View File

@@ -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();

View File

@@ -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 });
}
}

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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),

View File

@@ -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; }

View File

@@ -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");
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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");

View 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));
}
}
}

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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,

View File

@@ -114,7 +114,7 @@ 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)

View File

@@ -9,9 +9,11 @@
<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" />

View File

@@ -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;

View File

@@ -76,7 +76,7 @@ namespace Prowlarr.Api.V1.Indexers
{
_indexerService.DeleteIndexers(resource.IndexerIds);
return new object();
return new { };
}
}
}

View File

@@ -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;

View File

@@ -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")]

View File

@@ -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,

View File

@@ -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