mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-18 21:55:12 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d1a59008 | |||
| 33fa39dc84 | |||
| d133c82537 | |||
| 6b446e1404 | |||
| b0e879da5c | |||
| 5edde8d9bd | |||
| ef5d670c39 | |||
| f568906876 | |||
| 331e92ac62 | |||
| ec46b25be2 | |||
| 8b3837cb6e | |||
| ade5aee4a9 | |||
| c486013113 | |||
| c512cafb4a | |||
| 454641e8b5 | |||
| 7cac3fc174 |
+2
-2
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.3.3'
|
majorVersion: '2.3.5'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
@@ -17,7 +17,7 @@ variables:
|
|||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '8.0.405'
|
dotnetVersion: '8.0.405'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.7.1'
|
||||||
windowsImage: 'windows-2025'
|
windowsImage: 'windows-2025'
|
||||||
linuxImage: 'ubuntu-24.04'
|
linuxImage: 'ubuntu-24.04'
|
||||||
macImage: 'macOS-15'
|
macImage: 'macOS-15'
|
||||||
|
|||||||
@@ -253,8 +253,10 @@ InstallInno()
|
|||||||
{
|
{
|
||||||
ProgressStart "Installing portable Inno Setup"
|
ProgressStart "Installing portable Inno Setup"
|
||||||
|
|
||||||
|
INNOVERSION=${INNOVERSION:-6.7.1}
|
||||||
|
|
||||||
rm -rf _inno
|
rm -rf _inno
|
||||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
curl -s -L --output innosetup.exe "https://github.com/jrsoftware/issrc/releases/download/is-${INNOVERSION//./_}/innosetup-${INNOVERSION}.exe"
|
||||||
mkdir _inno
|
mkdir _inno
|
||||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||||
rm innosetup.exe
|
rm innosetup.exe
|
||||||
|
|||||||
@@ -5,6 +5,5 @@
|
|||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace NzbDrone.Common.Composition
|
|||||||
static AssemblyLoader()
|
static AssemblyLoader()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
|
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
|
||||||
RegisterSQLiteResolver();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IList<Assembly> Load(IList<string> assemblyNames)
|
public static IList<Assembly> Load(IList<string> assemblyNames)
|
||||||
@@ -23,6 +22,10 @@ namespace NzbDrone.Common.Composition
|
|||||||
toLoad.Add("Prowlarr.Common");
|
toLoad.Add("Prowlarr.Common");
|
||||||
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
||||||
|
|
||||||
|
var toRegisterResolver = new List<string> { "System.Data.SQLite" };
|
||||||
|
toRegisterResolver.AddRange(assemblyNames.Intersect(new[] { "Prowlarr.Core" }));
|
||||||
|
RegisterNativeResolver(toRegisterResolver);
|
||||||
|
|
||||||
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
return toLoad
|
return toLoad
|
||||||
@@ -43,27 +46,46 @@ namespace NzbDrone.Common.Composition
|
|||||||
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
|
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisterSQLiteResolver()
|
public static void RegisterNativeResolver(IEnumerable<string> assemblyNames)
|
||||||
{
|
{
|
||||||
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
|
foreach (var name in assemblyNames)
|
||||||
// is less likely to exist.
|
{
|
||||||
var sqliteAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
|
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
|
||||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "System.Data.SQLite.dll"));
|
// is less likely to exist.
|
||||||
|
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
|
||||||
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll"));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
NativeLibrary.SetDllImportResolver(sqliteAssembly, LoadSqliteNativeLib);
|
NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
// This can only be set once per assembly
|
// This can only be set once per assembly
|
||||||
// Catch required for NzbDrone.Host tests
|
// Catch required for NzbDrone.Host tests
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IntPtr LoadSqliteNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
|
private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
|
||||||
{
|
{
|
||||||
var mappedName = OsInfo.IsLinux && libraryName == "sqlite3" ? "libsqlite3.so.0" : libraryName;
|
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
|
||||||
|
|
||||||
|
var mappedName = libraryName;
|
||||||
|
|
||||||
|
if (libraryName is "sqlite3" or "e_sqlite3")
|
||||||
|
{
|
||||||
|
if (OsInfo.IsLinux)
|
||||||
|
{
|
||||||
|
if (NativeLibrary.TryLoad(libraryName, assembly, dllImportSearchPath, out var libHandle))
|
||||||
|
{
|
||||||
|
return libHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedName = "libsqlite3.so.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
|
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ using NzbDrone.Common.Extensions;
|
|||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
public class HttpUri : IEquatable<HttpUri>
|
public partial class HttpUri : IEquatable<HttpUri>
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
private readonly string _uri;
|
private readonly string _uri;
|
||||||
public string FullUri => _uri;
|
public string FullUri => _uri;
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||||
|
private static partial Regex UriRegex();
|
||||||
|
|
||||||
public HttpUri(string uri)
|
public HttpUri(string uri)
|
||||||
{
|
{
|
||||||
_uri = uri ?? string.Empty;
|
_uri = uri ?? string.Empty;
|
||||||
@@ -70,9 +71,9 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
private void Parse()
|
private void Parse()
|
||||||
{
|
{
|
||||||
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out _);
|
||||||
|
|
||||||
var match = RegexUri.Match(_uri);
|
var match = UriRegex().Match(_uri);
|
||||||
|
|
||||||
var scheme = match.Groups["scheme"];
|
var scheme = match.Groups["scheme"];
|
||||||
var host = match.Groups["host"];
|
var host = match.Groups["host"];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using NzbDrone.Common.Instrumentation;
|
|||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||||
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
public class DatabaseEngineVersionCheck : ForwardOnlyMigration
|
||||||
{
|
{
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
@@ -22,11 +22,6 @@ namespace NzbDrone.Core.Datastore.Migration
|
|||||||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Down()
|
|
||||||
{
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||||
{
|
{
|
||||||
using (var versionCmd = conn.CreateCommand())
|
using (var versionCmd = conn.CreateCommand())
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using FluentMigrator.Runner.Generators;
|
|||||||
using FluentMigrator.Runner.Initialization;
|
using FluentMigrator.Runner.Initialization;
|
||||||
using FluentMigrator.Runner.Processors;
|
using FluentMigrator.Runner.Processors;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
|
|
||||||
@@ -20,13 +19,10 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly ILoggerProvider _migrationLoggerProvider;
|
|
||||||
|
|
||||||
public MigrationController(Logger logger,
|
public MigrationController(Logger logger)
|
||||||
ILoggerProvider migrationLoggerProvider)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_migrationLoggerProvider = migrationLoggerProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
@@ -35,16 +31,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||||
|
|
||||||
ServiceProvider serviceProvider;
|
|
||||||
|
|
||||||
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||||
|
|
||||||
serviceProvider = new ServiceCollection()
|
var serviceProvider = new ServiceCollection()
|
||||||
.AddLogging(b => b.AddNLog())
|
.AddLogging(b => b.AddNLog())
|
||||||
.AddFluentMigratorCore()
|
.AddFluentMigratorCore()
|
||||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||||
.ConfigureRunner(
|
.ConfigureRunner(builder => builder
|
||||||
builder => builder
|
|
||||||
.AddPostgres()
|
.AddPostgres()
|
||||||
.AddNzbDroneSQLite()
|
.AddNzbDroneSQLite()
|
||||||
.WithGlobalConnectionString(connectionString)
|
.WithGlobalConnectionString(connectionString)
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using FluentMigrator;
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
using FluentMigrator.Builders.Create;
|
using FluentMigrator.Builders.Create;
|
||||||
using FluentMigrator.Builders.Create.Table;
|
using FluentMigrator.Builders.Create.Table;
|
||||||
using FluentMigrator.Runner;
|
using FluentMigrator.Runner;
|
||||||
using FluentMigrator.Runner.BatchParser;
|
using FluentMigrator.Runner.BatchParser;
|
||||||
|
using FluentMigrator.Runner.Generators;
|
||||||
using FluentMigrator.Runner.Generators.SQLite;
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
using FluentMigrator.Runner.Initialization;
|
||||||
|
using FluentMigrator.Runner.Processors;
|
||||||
using FluentMigrator.Runner.Processors.SQLite;
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
{
|
{
|
||||||
@@ -16,23 +22,49 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddParameter(this System.Data.IDbCommand command, object value)
|
public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction tran, string query)
|
||||||
|
{
|
||||||
|
var command = conn.CreateCommand();
|
||||||
|
command.Transaction = tran;
|
||||||
|
command.CommandText = query;
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddParameter(this IDbCommand command, object value)
|
||||||
{
|
{
|
||||||
var parameter = command.CreateParameter();
|
var parameter = command.CreateParameter();
|
||||||
parameter.Value = value;
|
parameter.Value = value;
|
||||||
command.Parameters.Add(parameter);
|
command.Parameters.Add(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder)
|
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder, bool binaryGuid = false, bool useStrictTables = false)
|
||||||
{
|
{
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddTransient<SQLiteBatchParser>()
|
.AddTransient<SQLiteBatchParser>()
|
||||||
.AddScoped<SQLiteDbFactory>()
|
.AddScoped<SQLiteDbFactory>()
|
||||||
.AddScoped<NzbDroneSQLiteProcessor>()
|
.AddScoped<NzbDroneSQLiteProcessor>(sp =>
|
||||||
|
{
|
||||||
|
var factory = sp.GetService<SQLiteDbFactory>();
|
||||||
|
var logger = sp.GetService<ILogger<NzbDroneSQLiteProcessor>>();
|
||||||
|
var options = sp.GetService<IOptionsSnapshot<ProcessorOptions>>();
|
||||||
|
var connectionStringAccessor = sp.GetService<IConnectionStringAccessor>();
|
||||||
|
var sqliteQuoter = new SQLiteQuoter(false);
|
||||||
|
return new NzbDroneSQLiteProcessor(factory, sp.GetService<SQLiteGenerator>(), logger, options, connectionStringAccessor, sp, sqliteQuoter);
|
||||||
|
})
|
||||||
|
.AddScoped<ISQLiteTypeMap>(_ => new NzbDroneSQLiteTypeMap(useStrictTables))
|
||||||
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
||||||
.AddScoped<SQLiteQuoter>()
|
.AddScoped(
|
||||||
.AddScoped<SQLiteGenerator>()
|
sp =>
|
||||||
|
{
|
||||||
|
var typeMap = sp.GetRequiredService<ISQLiteTypeMap>();
|
||||||
|
return new SQLiteGenerator(
|
||||||
|
new SQLiteQuoter(binaryGuid),
|
||||||
|
typeMap,
|
||||||
|
new OptionsWrapper<GeneratorOptions>(new GeneratorOptions()));
|
||||||
|
})
|
||||||
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
{
|
{
|
||||||
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
||||||
{
|
{
|
||||||
|
private readonly SQLiteQuoter _quoter;
|
||||||
|
|
||||||
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
||||||
SQLiteGenerator generator,
|
SQLiteGenerator generator,
|
||||||
ILogger<NzbDroneSQLiteProcessor> logger,
|
ILogger<NzbDroneSQLiteProcessor> logger,
|
||||||
@@ -24,6 +26,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
SQLiteQuoter quoter)
|
SQLiteQuoter quoter)
|
||||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
||||||
{
|
{
|
||||||
|
_quoter = quoter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Process(AlterColumnExpression expression)
|
public override void Process(AlterColumnExpression expression)
|
||||||
@@ -35,7 +38,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
|
throw new ApplicationException($"Column {expression.Column.Name} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
columnDefinitions[columnIndex] = expression.Column;
|
columnDefinitions[columnIndex] = expression.Column;
|
||||||
@@ -45,6 +48,28 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Process(AlterDefaultConstraintExpression expression)
|
||||||
|
{
|
||||||
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
|
|
||||||
|
var columnDefinitions = tableDefinition.Columns.ToList();
|
||||||
|
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.ColumnName);
|
||||||
|
|
||||||
|
if (columnIndex == -1)
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"Column {expression.ColumnName} does not exist on table {expression.TableName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var changedColumn = columnDefinitions[columnIndex];
|
||||||
|
changedColumn.DefaultValue = expression.DefaultValue;
|
||||||
|
|
||||||
|
columnDefinitions[columnIndex] = changedColumn;
|
||||||
|
|
||||||
|
tableDefinition.Columns = columnDefinitions;
|
||||||
|
|
||||||
|
ProcessAlterTable(tableDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
public override void Process(DeleteColumnExpression expression)
|
public override void Process(DeleteColumnExpression expression)
|
||||||
{
|
{
|
||||||
var tableDefinition = GetTableSchema(expression.TableName);
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
@@ -62,7 +87,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnsToRemove.Any())
|
if (columnsToRemove.Any())
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
|
throw new ApplicationException($"Column {columnsToRemove.First()} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
@@ -78,12 +103,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName));
|
throw new ApplicationException($"Column {expression.OldName} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName));
|
throw new ApplicationException($"Column {expression.NewName} already exists on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
||||||
@@ -128,21 +153,20 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// What is the cleanest way to do this? Add function to Generator?
|
// What is the cleanest way to do this? Add function to Generator?
|
||||||
var quoter = new SQLiteQuoter();
|
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||||
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
|
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||||
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name)));
|
|
||||||
|
|
||||||
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
Process(new CreateTableExpression { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||||
|
|
||||||
Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName)));
|
Process($"INSERT INTO {_quoter.QuoteTableName(tempTableName)} ({columnsToInsert}) SELECT {columnsToFetch} FROM {_quoter.QuoteTableName(tableName)}");
|
||||||
|
|
||||||
Process(new DeleteTableExpression() { TableName = tableName });
|
Process(new DeleteTableExpression { TableName = tableName });
|
||||||
|
|
||||||
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
|
Process(new RenameTableExpression { OldName = tempTableName, NewName = tableName });
|
||||||
|
|
||||||
foreach (var index in tableDefinition.Indexes)
|
foreach (var index in tableDefinition.Indexes)
|
||||||
{
|
{
|
||||||
Process(new CreateIndexExpression() { Index = index });
|
Process(new CreateIndexExpression { Index = index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Data;
|
||||||
|
using FluentMigrator.Runner.Generators.Base;
|
||||||
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
// Based on https://github.com/fluentmigrator/fluentmigrator/blob/v6.2.0/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteTypeMap.cs
|
||||||
|
public sealed class NzbDroneSQLiteTypeMap : TypeMapBase, ISQLiteTypeMap
|
||||||
|
{
|
||||||
|
public bool UseStrictTables { get; }
|
||||||
|
|
||||||
|
public NzbDroneSQLiteTypeMap(bool useStrictTables = false)
|
||||||
|
{
|
||||||
|
UseStrictTables = useStrictTables;
|
||||||
|
|
||||||
|
SetupTypeMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be kept in sync with upstream
|
||||||
|
protected override void SetupTypeMaps()
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Binary, "BLOB");
|
||||||
|
SetTypeMap(DbType.Byte, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int16, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int32, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int64, "INTEGER");
|
||||||
|
SetTypeMap(DbType.SByte, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt16, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt32, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt64, "INTEGER");
|
||||||
|
|
||||||
|
if (!UseStrictTables)
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Currency, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Decimal, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Double, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Single, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.VarNumeric, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Date, "DATETIME");
|
||||||
|
SetTypeMap(DbType.DateTime, "DATETIME");
|
||||||
|
SetTypeMap(DbType.DateTime2, "DATETIME");
|
||||||
|
SetTypeMap(DbType.Time, "DATETIME");
|
||||||
|
SetTypeMap(DbType.Guid, "UNIQUEIDENTIFIER");
|
||||||
|
|
||||||
|
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||||
|
SetTypeMap(DbType.DateTimeOffset, "DATETIME");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Currency, "TEXT");
|
||||||
|
SetTypeMap(DbType.Decimal, "TEXT");
|
||||||
|
SetTypeMap(DbType.Double, "REAL");
|
||||||
|
SetTypeMap(DbType.Single, "REAL");
|
||||||
|
SetTypeMap(DbType.VarNumeric, "TEXT");
|
||||||
|
SetTypeMap(DbType.Date, "TEXT");
|
||||||
|
SetTypeMap(DbType.DateTime, "TEXT");
|
||||||
|
SetTypeMap(DbType.DateTime2, "TEXT");
|
||||||
|
SetTypeMap(DbType.Time, "TEXT");
|
||||||
|
SetTypeMap(DbType.Guid, "TEXT");
|
||||||
|
|
||||||
|
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||||
|
SetTypeMap(DbType.DateTimeOffset, "TEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTypeMap(DbType.AnsiString, "TEXT");
|
||||||
|
SetTypeMap(DbType.String, "TEXT");
|
||||||
|
SetTypeMap(DbType.AnsiStringFixedLength, "TEXT");
|
||||||
|
SetTypeMap(DbType.StringFixedLength, "TEXT");
|
||||||
|
SetTypeMap(DbType.Boolean, "INTEGER");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetTypeMap(DbType type, int? size, int? precision)
|
||||||
|
{
|
||||||
|
return base.GetTypeMap(type, size: null, precision: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using AngleSharp.Html.Parser;
|
|
||||||
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.Indexers.Settings;
|
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
|
||||||
using NzbDrone.Core.Messaging.Events;
|
|
||||||
using NzbDrone.Core.Parser;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Definitions
|
|
||||||
{
|
|
||||||
public class AnimeTorrents : TorrentIndexerBase<AnimeTorrentsSettings>
|
|
||||||
{
|
|
||||||
public override string Name => "AnimeTorrents";
|
|
||||||
public override string[] IndexerUrls => new[] { "https://animetorrents.me/" };
|
|
||||||
public override string Description => "Definitive source for anime and manga";
|
|
||||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
|
||||||
public override bool SupportsPagination => true;
|
|
||||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(4);
|
|
||||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
|
||||||
|
|
||||||
public AnimeTorrents(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
|
||||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
|
||||||
{
|
|
||||||
return new AnimeTorrentsRequestGenerator(Settings, Capabilities);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IParseIndexerResponse GetParser()
|
|
||||||
{
|
|
||||||
return new AnimeTorrentsParser(Settings, Capabilities.Categories);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
|
||||||
{
|
|
||||||
if (httpResponse.Content.Contains("Access Denied!") || httpResponse.Content.Contains("login.php"))
|
|
||||||
{
|
|
||||||
throw new IndexerAuthException("AnimeTorrents authentication with cookies failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IDictionary<string, string> GetCookies()
|
|
||||||
{
|
|
||||||
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IndexerCapabilities SetCapabilities()
|
|
||||||
{
|
|
||||||
var caps = new IndexerCapabilities
|
|
||||||
{
|
|
||||||
TvSearchParams = new List<TvSearchParam>
|
|
||||||
{
|
|
||||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
|
||||||
},
|
|
||||||
MovieSearchParams = new List<MovieSearchParam>
|
|
||||||
{
|
|
||||||
MovieSearchParam.Q
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.MoviesSD, "Anime Movie");
|
|
||||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.MoviesHD, "Anime Movie HD");
|
|
||||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Anime Series");
|
|
||||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TVAnime, "Anime Series HD");
|
|
||||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.XXXDVD, "Hentai (censored)");
|
|
||||||
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.XXXDVD, "Hentai (censored) HD");
|
|
||||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.XXXDVD, "Hentai (un-censored)");
|
|
||||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.XXXDVD, "Hentai (un-censored) HD");
|
|
||||||
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.BooksForeign, "Light Novel");
|
|
||||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksComics, "Manga");
|
|
||||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.BooksComics, "Manga 18+");
|
|
||||||
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.TVAnime, "OVA");
|
|
||||||
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.TVAnime, "OVA HD");
|
|
||||||
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.BooksComics, "Doujin Anime");
|
|
||||||
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.XXXDVD, "Doujin Anime 18+");
|
|
||||||
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.AudioForeign, "Doujin Music");
|
|
||||||
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.BooksComics, "Doujinshi");
|
|
||||||
caps.Categories.AddCategoryMapping(18, NewznabStandardCategory.BooksComics, "Doujinshi 18+");
|
|
||||||
caps.Categories.AddCategoryMapping(19, NewznabStandardCategory.Audio, "OST");
|
|
||||||
caps.Categories.AddCategoryMapping(20, NewznabStandardCategory.AudioAudiobook, "Audiobooks");
|
|
||||||
|
|
||||||
return caps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnimeTorrentsRequestGenerator : IIndexerRequestGenerator
|
|
||||||
{
|
|
||||||
private readonly AnimeTorrentsSettings _settings;
|
|
||||||
private readonly IndexerCapabilities _capabilities;
|
|
||||||
|
|
||||||
public AnimeTorrentsRequestGenerator(AnimeTorrentsSettings settings, IndexerCapabilities capabilities)
|
|
||||||
{
|
|
||||||
_settings = settings;
|
|
||||||
_capabilities = capabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
|
||||||
{
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
|
||||||
|
|
||||||
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
|
|
||||||
|
|
||||||
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
|
|
||||||
{
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageableRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
|
||||||
{
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
|
||||||
|
|
||||||
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
|
|
||||||
|
|
||||||
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
|
|
||||||
{
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageableRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
|
||||||
{
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
|
||||||
|
|
||||||
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
|
|
||||||
|
|
||||||
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
|
|
||||||
{
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageableRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
|
||||||
{
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
|
||||||
|
|
||||||
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
|
|
||||||
|
|
||||||
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
|
|
||||||
{
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageableRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
|
||||||
{
|
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
|
||||||
|
|
||||||
var searchTerm = $"{searchCriteria.SanitizedSearchTerm}";
|
|
||||||
|
|
||||||
foreach (var category in GetTrackerCategories(searchTerm, searchCriteria))
|
|
||||||
{
|
|
||||||
pageableRequests.Add(GetPagedRequests(searchTerm, category, searchCriteria));
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageableRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, string category, SearchCriteriaBase searchCriteria)
|
|
||||||
{
|
|
||||||
var searchUrl = _settings.BaseUrl + "ajax/torrents_data.php";
|
|
||||||
|
|
||||||
// replace non-word characters with % (wildcard)
|
|
||||||
var searchString = Regex.Replace(term.Trim(), @"[\W]+", "%");
|
|
||||||
|
|
||||||
var page = searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0 ? (int)(searchCriteria.Offset / searchCriteria.Limit) + 1 : 1;
|
|
||||||
|
|
||||||
var refererUri = new HttpUri(_settings.BaseUrl)
|
|
||||||
.CombinePath("/torrents.php")
|
|
||||||
.AddQueryParam("cat", $"{category}");
|
|
||||||
|
|
||||||
if (_settings.DownloadableOnly)
|
|
||||||
{
|
|
||||||
refererUri = refererUri.AddQueryParam("dlable", "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestBuilder = new HttpRequestBuilder(searchUrl)
|
|
||||||
.AddQueryParam("total", "100") // Assuming the total number of pages
|
|
||||||
.AddQueryParam("cat", $"{category}")
|
|
||||||
.AddQueryParam("searchin", "filename")
|
|
||||||
.AddQueryParam("search", searchString)
|
|
||||||
.AddQueryParam("page", page)
|
|
||||||
.SetHeader("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.SetHeader("Referer", refererUri.FullUri)
|
|
||||||
.Accept(HttpAccept.Html);
|
|
||||||
|
|
||||||
if (_settings.DownloadableOnly)
|
|
||||||
{
|
|
||||||
requestBuilder.AddQueryParam("dlable", "1");
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return new IndexerRequest(requestBuilder.Build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> GetTrackerCategories(string term, SearchCriteriaBase searchCriteria)
|
|
||||||
{
|
|
||||||
var searchTerm = term.Trim();
|
|
||||||
|
|
||||||
var categoryMapping = _capabilities.Categories
|
|
||||||
.MapTorznabCapsToTrackers(searchCriteria.Categories)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return searchTerm.IsNullOrWhiteSpace() && categoryMapping.Count == 2
|
|
||||||
? categoryMapping
|
|
||||||
: new List<string> { categoryMapping.FirstIfSingleOrDefault("0") };
|
|
||||||
}
|
|
||||||
|
|
||||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
|
||||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnimeTorrentsParser : IParseIndexerResponse
|
|
||||||
{
|
|
||||||
private readonly AnimeTorrentsSettings _settings;
|
|
||||||
private readonly IndexerCapabilitiesCategories _categories;
|
|
||||||
|
|
||||||
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories)
|
|
||||||
{
|
|
||||||
_settings = settings;
|
|
||||||
_categories = categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|
||||||
{
|
|
||||||
var releaseInfos = new List<ReleaseInfo>();
|
|
||||||
|
|
||||||
var parser = new HtmlParser();
|
|
||||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
|
||||||
|
|
||||||
var rows = dom.QuerySelectorAll("table tr");
|
|
||||||
foreach (var (row, index) in rows.Skip(1).Select((v, i) => (v, i)))
|
|
||||||
{
|
|
||||||
var downloadVolumeFactor = row.QuerySelector("img[alt=\"Gold Torrent\"]") != null ? 0 : row.QuerySelector("img[alt=\"Silver Torrent\"]") != null ? 0.5 : 1;
|
|
||||||
|
|
||||||
// skip non-freeleech results when freeleech only is set
|
|
||||||
if (_settings.FreeleechOnly && downloadVolumeFactor != 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var qTitleLink = row.QuerySelector("td:nth-of-type(2) a:nth-of-type(1)");
|
|
||||||
var title = qTitleLink?.TextContent.Trim();
|
|
||||||
|
|
||||||
// If we search and get no results, we still get a table just with no info.
|
|
||||||
if (title.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var infoUrl = qTitleLink?.GetAttribute("href");
|
|
||||||
|
|
||||||
// newbie users don't see DL links
|
|
||||||
// use details link as placeholder
|
|
||||||
// skipping the release prevents newbie users from adding the tracker (empty result)
|
|
||||||
var downloadUrl = row.QuerySelector("td:nth-of-type(3) a")?.GetAttribute("href") ?? infoUrl;
|
|
||||||
|
|
||||||
var connections = row.QuerySelector("td:nth-of-type(8)").TextContent.Trim().Split('/', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var seeders = ParseUtil.CoerceInt(connections[0]);
|
|
||||||
var leechers = ParseUtil.CoerceInt(connections[1]);
|
|
||||||
var grabs = ParseUtil.CoerceInt(connections[2]);
|
|
||||||
|
|
||||||
var categoryLink = row.QuerySelector("td:nth-of-type(1) a")?.GetAttribute("href") ?? string.Empty;
|
|
||||||
var categoryId = ParseUtil.GetArgumentFromQueryString(categoryLink, "cat");
|
|
||||||
|
|
||||||
var publishedDate = DateTime.ParseExact(row.QuerySelector("td:nth-of-type(5)").TextContent, "dd MMM yy", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
|
|
||||||
|
|
||||||
if (publishedDate.Date == DateTime.Today)
|
|
||||||
{
|
|
||||||
publishedDate = publishedDate.Date + DateTime.Now.TimeOfDay - TimeSpan.FromMinutes(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
var release = new TorrentInfo
|
|
||||||
{
|
|
||||||
Guid = infoUrl,
|
|
||||||
InfoUrl = infoUrl,
|
|
||||||
DownloadUrl = downloadUrl,
|
|
||||||
Title = title,
|
|
||||||
Categories = _categories.MapTrackerCatToNewznab(categoryId),
|
|
||||||
PublishDate = publishedDate,
|
|
||||||
Size = ParseUtil.GetBytes(row.QuerySelector("td:nth-of-type(6)").TextContent.Trim()),
|
|
||||||
Seeders = seeders,
|
|
||||||
Peers = leechers + seeders,
|
|
||||||
Grabs = grabs,
|
|
||||||
DownloadVolumeFactor = downloadVolumeFactor,
|
|
||||||
UploadVolumeFactor = 1,
|
|
||||||
Genres = row.QuerySelectorAll("td:nth-of-type(2) a.tortags").Select(t => t.TextContent.Trim()).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
var uploadFactor = row.QuerySelector("img[alt*=\"x Multiplier Torrent\"]")?.GetAttribute("alt");
|
|
||||||
if (uploadFactor != null)
|
|
||||||
{
|
|
||||||
release.UploadVolumeFactor = ParseUtil.CoerceDouble(uploadFactor.Split('x')[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
releaseInfos.Add(release);
|
|
||||||
}
|
|
||||||
|
|
||||||
return releaseInfos.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnimeTorrentsSettings : CookieTorrentBaseSettings
|
|
||||||
{
|
|
||||||
public AnimeTorrentsSettings()
|
|
||||||
{
|
|
||||||
FreeleechOnly = false;
|
|
||||||
DownloadableOnly = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[FieldDefinition(4, Label = "Freeleech Only", Type = FieldType.Checkbox, HelpText = "Show freeleech torrents only")]
|
|
||||||
public bool FreeleechOnly { get; set; }
|
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Downloadable Only", Type = FieldType.Checkbox, HelpText = "Search downloadable torrents only (enable this only if your account class is Newbie)", Advanced = true)]
|
|
||||||
public bool DownloadableOnly { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Exceptions;
|
||||||
|
using NzbDrone.Core.Indexers.Definitions.Avistaz;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Definitions;
|
||||||
|
|
||||||
|
public class AnimeZ : AvistazBase
|
||||||
|
{
|
||||||
|
public override string Name => "AnimeZ";
|
||||||
|
public override string[] IndexerUrls => new[] { "https://animez.to/" };
|
||||||
|
public override string Description => "AnimeZ (ex-AnimeTorrents) is a Private Torrent Tracker for ANIME / MANGA";
|
||||||
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||||
|
|
||||||
|
public AnimeZ(IIndexerRepository indexerRepository,
|
||||||
|
IIndexerHttpClient httpClient,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
IIndexerStatusService indexerStatusService,
|
||||||
|
IConfigService configService,
|
||||||
|
Logger logger)
|
||||||
|
: base(indexerRepository, httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||||
|
{
|
||||||
|
return new AnimeZRequestGenerator
|
||||||
|
{
|
||||||
|
Settings = Settings,
|
||||||
|
Capabilities = Capabilities,
|
||||||
|
PageSize = PageSize,
|
||||||
|
HttpClient = _httpClient,
|
||||||
|
Logger = _logger
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IParseIndexerResponse GetParser()
|
||||||
|
{
|
||||||
|
return new AnimeZParser(Capabilities.Categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await base.Download(link).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (ReleaseDownloadException ex) when (ex.InnerException is HttpException httpException &&
|
||||||
|
httpException.Response.StatusCode is HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
await DoLogin().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.Download(link).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<HttpRequest> GetDownloadRequest(Uri link)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestBuilder(link.AbsoluteUri)
|
||||||
|
.Accept(HttpAccept.Json)
|
||||||
|
.SetHeader("Authorization", $"Bearer {Settings.Token}")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return Task.FromResult(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IndexerCapabilities SetCapabilities()
|
||||||
|
{
|
||||||
|
var caps = new IndexerCapabilities
|
||||||
|
{
|
||||||
|
LimitsDefault = PageSize,
|
||||||
|
LimitsMax = PageSize,
|
||||||
|
TvSearchParams = new List<TvSearchParam>
|
||||||
|
{
|
||||||
|
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||||
|
},
|
||||||
|
MovieSearchParams = new List<MovieSearchParam>
|
||||||
|
{
|
||||||
|
MovieSearchParam.Q
|
||||||
|
},
|
||||||
|
BookSearchParams = new List<BookSearchParam>
|
||||||
|
{
|
||||||
|
BookSearchParam.Q,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
caps.Categories.AddCategoryMapping("TV", NewznabStandardCategory.TVAnime, "Anime > TV");
|
||||||
|
caps.Categories.AddCategoryMapping("TV_SHORT", NewznabStandardCategory.TVAnime, "Anime > TV Short");
|
||||||
|
caps.Categories.AddCategoryMapping("MOVIE", NewznabStandardCategory.Movies, "Anime > Movie");
|
||||||
|
caps.Categories.AddCategoryMapping("SPECIAL", NewznabStandardCategory.TVAnime, "Anime > Special");
|
||||||
|
caps.Categories.AddCategoryMapping("OVA", NewznabStandardCategory.TVAnime, "Anime > OVA");
|
||||||
|
caps.Categories.AddCategoryMapping("ONA", NewznabStandardCategory.TVAnime, "Anime > ONA");
|
||||||
|
caps.Categories.AddCategoryMapping("MUSIC", NewznabStandardCategory.TVAnime, "Anime > Music");
|
||||||
|
caps.Categories.AddCategoryMapping("MANGA", NewznabStandardCategory.BooksComics, "Manga > Manga");
|
||||||
|
caps.Categories.AddCategoryMapping("NOVEL", NewznabStandardCategory.BooksForeign, "Manga > Novel");
|
||||||
|
caps.Categories.AddCategoryMapping("ONE_SHOT", NewznabStandardCategory.BooksForeign, "Manga > One-Shot");
|
||||||
|
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnimeZRequestGenerator : AvistazRequestGenerator
|
||||||
|
{
|
||||||
|
protected override List<KeyValuePair<string, string>> GetBasicSearchParameters(SearchCriteriaBase searchCriteria, string genre = null)
|
||||||
|
{
|
||||||
|
var parameters = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
{ "limit", Math.Min(PageSize, searchCriteria.Limit.GetValueOrDefault(PageSize)).ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
var categoryMappings = Capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
|
||||||
|
|
||||||
|
if (categoryMappings.Any())
|
||||||
|
{
|
||||||
|
foreach (var category in categoryMappings)
|
||||||
|
{
|
||||||
|
parameters.Add("format[]", category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCriteria.Limit is > 0 && searchCriteria.Offset is > 0)
|
||||||
|
{
|
||||||
|
var page = (int)(searchCriteria.Offset / searchCriteria.Limit) + 1;
|
||||||
|
parameters.Add("page", page.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.FreeleechOnly)
|
||||||
|
{
|
||||||
|
parameters.Add("freeleech", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnimeZParser(IndexerCapabilitiesCategories categories) : AvistazParserBase
|
||||||
|
{
|
||||||
|
protected override List<IndexerCategory> ParseCategories(AvistazRelease row)
|
||||||
|
{
|
||||||
|
return categories.MapTrackerCatToNewznab(row.Format).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string ParseTitle(AvistazRelease row)
|
||||||
|
{
|
||||||
|
return row.ReleaseTitle.IsNotNullOrWhiteSpace() ? row.ReleaseTitle : row.FileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,72 +5,77 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
public class AvistazRelease
|
public class AvistazRelease
|
||||||
{
|
{
|
||||||
public string Url { get; set; }
|
public string Url { get; init; }
|
||||||
public string Download { get; set; }
|
public string Download { get; init; }
|
||||||
public Dictionary<string, string> Category { get; set; }
|
public Dictionary<string, string> Category { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("movie_tv")]
|
[JsonPropertyName("movie_tv")]
|
||||||
public AvistazIdInfo MovieTvinfo { get; set; }
|
public AvistazIdInfo MovieTvinfo { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("created_at")]
|
|
||||||
public string CreatedAt { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("created_at_iso")]
|
[JsonPropertyName("created_at_iso")]
|
||||||
public string CreatedAtIso { get; set; }
|
public string CreatedAtIso { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("file_name")]
|
[JsonPropertyName("file_name")]
|
||||||
public string FileName { get; set; }
|
public string FileName { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("release_title")]
|
||||||
|
public string ReleaseTitle { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("info_hash")]
|
[JsonPropertyName("info_hash")]
|
||||||
public string InfoHash { get; set; }
|
public string InfoHash { get; init; }
|
||||||
public int? Leech { get; set; }
|
|
||||||
public int? Completed { get; set; }
|
public int? Leech { get; init; }
|
||||||
public int? Seed { get; set; }
|
public int? Completed { get; init; }
|
||||||
|
public int? Seed { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("file_size")]
|
[JsonPropertyName("file_size")]
|
||||||
public long? FileSize { get; set; }
|
public long? FileSize { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("file_count")]
|
[JsonPropertyName("file_count")]
|
||||||
public int? FileCount { get; set; }
|
public int? FileCount { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("download_multiply")]
|
[JsonPropertyName("download_multiply")]
|
||||||
public double? DownloadMultiply { get; set; }
|
public double? DownloadMultiply { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("upload_multiply")]
|
[JsonPropertyName("upload_multiply")]
|
||||||
public double? UploadMultiply { get; set; }
|
public double? UploadMultiply { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("video_quality")]
|
[JsonPropertyName("video_quality")]
|
||||||
public string VideoQuality { get; set; }
|
public string VideoQuality { get; init; }
|
||||||
public string Type { get; set; }
|
|
||||||
public List<AvistazLanguage> Audio { get; set; }
|
public string Type { get; init; }
|
||||||
public List<AvistazLanguage> Subtitle { get; set; }
|
|
||||||
|
public string Format { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<AvistazLanguage> Audio { get; init; }
|
||||||
|
public IReadOnlyCollection<AvistazLanguage> Subtitle { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvistazLanguage
|
public class AvistazLanguage
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; init; }
|
||||||
public string Language { get; set; }
|
public string Language { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvistazResponse
|
public class AvistazResponse
|
||||||
{
|
{
|
||||||
public List<AvistazRelease> Data { get; set; }
|
public IReadOnlyCollection<AvistazRelease> Data { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvistazErrorResponse
|
public class AvistazErrorResponse
|
||||||
{
|
{
|
||||||
public string Message { get; set; }
|
public string Message { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvistazIdInfo
|
public class AvistazIdInfo
|
||||||
{
|
{
|
||||||
public string Tmdb { get; set; }
|
public string Tmdb { get; init; }
|
||||||
public string Tvdb { get; set; }
|
public string Tvdb { get; init; }
|
||||||
public string Imdb { get; set; }
|
public string Imdb { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvistazAuthResponse
|
public class AvistazAuthResponse
|
||||||
{
|
{
|
||||||
public string Token { get; set; }
|
public string Token { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
@@ -21,7 +20,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(6);
|
public override TimeSpan RateLimit => TimeSpan.FromSeconds(6);
|
||||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||||
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
|
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
|
||||||
private IIndexerRepository _indexerRepository;
|
private readonly IIndexerRepository _indexerRepository;
|
||||||
|
|
||||||
public AvistazBase(IIndexerRepository indexerRepository,
|
public AvistazBase(IIndexerRepository indexerRepository,
|
||||||
IIndexerHttpClient httpClient,
|
IIndexerHttpClient httpClient,
|
||||||
@@ -57,7 +56,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Settings.Token = await GetToken();
|
Settings.Token = await GetToken().ConfigureAwait(false);
|
||||||
|
|
||||||
if (Definition.Id > 0)
|
if (Definition.Id > 0)
|
||||||
{
|
{
|
||||||
@@ -66,7 +65,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
|
|
||||||
_logger.Debug("Avistaz authentication succeeded.");
|
_logger.Debug("Avistaz authentication succeeded.");
|
||||||
}
|
}
|
||||||
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
catch (HttpException ex) when (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.UnprocessableEntity)
|
||||||
{
|
{
|
||||||
_logger.Warn(ex, "Failed to authenticate with Avistaz");
|
_logger.Warn(ex, "Failed to authenticate with Avistaz");
|
||||||
|
|
||||||
@@ -90,11 +89,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetToken();
|
await GetToken().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (HttpException ex)
|
catch (HttpException ex)
|
||||||
{
|
{
|
||||||
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.UnprocessableEntity)
|
||||||
{
|
{
|
||||||
_logger.Warn(ex, "Unauthorized request to indexer");
|
_logger.Warn(ex, "Unauthorized request to indexer");
|
||||||
|
|
||||||
@@ -110,10 +109,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
_logger.Warn(ex, "Unable to connect to indexer");
|
_logger.Warn(ex, "Unable to connect to indexer");
|
||||||
|
|
||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details");
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log above the ValidationFailure for more details. " + ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await base.TestConnection();
|
return await base.TestConnection().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetToken()
|
private async Task<string> GetToken()
|
||||||
@@ -121,18 +120,17 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||||
{
|
{
|
||||||
LogResponseContent = true,
|
LogResponseContent = true,
|
||||||
Method = HttpMethod.Post
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
|
|
||||||
var authLoginRequest = requestBuilder
|
var authLoginRequest = requestBuilder
|
||||||
|
.Post()
|
||||||
.AddFormParameter("username", Settings.Username)
|
.AddFormParameter("username", Settings.Username)
|
||||||
.AddFormParameter("password", Settings.Password)
|
.AddFormParameter("password", Settings.Password)
|
||||||
.AddFormParameter("pid", Settings.Pid.Trim())
|
.AddFormParameter("pid", Settings.Pid.Trim())
|
||||||
.Accept(HttpAccept.Html)
|
.Accept(HttpAccept.Json)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var response = await ExecuteAuth(authLoginRequest);
|
var response = await ExecuteAuth(authLoginRequest).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!STJson.TryDeserialize<AvistazAuthResponse>(response.Content, out var authResponse))
|
if (!STJson.TryDeserialize<AvistazAuthResponse>(response.Content, out var authResponse))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
|
|
||||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
|
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||||
{
|
{
|
||||||
return releaseInfos.ToArray();
|
return releaseInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
@@ -52,31 +52,28 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
|
|
||||||
foreach (var row in jsonResponse.Data)
|
foreach (var row in jsonResponse.Data)
|
||||||
{
|
{
|
||||||
var details = row.Url;
|
var detailsUrl = row.Url;
|
||||||
var link = row.Download;
|
|
||||||
|
|
||||||
var cats = ParseCategories(row);
|
|
||||||
|
|
||||||
var release = new TorrentInfo
|
var release = new TorrentInfo
|
||||||
{
|
{
|
||||||
Title = row.FileName,
|
Guid = detailsUrl,
|
||||||
DownloadUrl = link,
|
InfoUrl = detailsUrl,
|
||||||
|
Title = ParseTitle(row),
|
||||||
|
DownloadUrl = row.Download,
|
||||||
|
Categories = ParseCategories(row).ToList(),
|
||||||
InfoHash = row.InfoHash,
|
InfoHash = row.InfoHash,
|
||||||
InfoUrl = details,
|
|
||||||
Guid = details,
|
|
||||||
Categories = cats,
|
|
||||||
PublishDate = DateTime.Parse(row.CreatedAtIso, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
|
||||||
Size = row.FileSize,
|
Size = row.FileSize,
|
||||||
Files = row.FileCount,
|
Files = row.FileCount,
|
||||||
Grabs = row.Completed,
|
Grabs = row.Completed,
|
||||||
Seeders = row.Seed,
|
Seeders = row.Seed,
|
||||||
Peers = row.Leech + row.Seed,
|
Peers = row.Leech + row.Seed,
|
||||||
|
PublishDate = DateTime.Parse(row.CreatedAtIso, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||||
DownloadVolumeFactor = row.DownloadMultiply,
|
DownloadVolumeFactor = row.DownloadMultiply,
|
||||||
UploadVolumeFactor = row.UploadMultiply,
|
UploadVolumeFactor = row.UploadMultiply,
|
||||||
MinimumRatio = 1,
|
MinimumRatio = 1,
|
||||||
MinimumSeedTime = 259200, // 72 hours
|
MinimumSeedTime = 259200, // 72 hours
|
||||||
Languages = row.Audio?.Select(x => x.Language).ToList() ?? new List<string>(),
|
Languages = row.Audio?.Select(x => x.Language).ToList() ?? [],
|
||||||
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? new List<string>()
|
Subs = row.Subtitle?.Select(x => x.Language).ToList() ?? []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (row.FileSize is > 0)
|
if (row.FileSize is > 0)
|
||||||
@@ -90,54 +87,57 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.MovieTvinfo != null)
|
if (row.MovieTvinfo is not null)
|
||||||
{
|
{
|
||||||
release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault();
|
release.ImdbId = ParseUtil.GetImdbId(row.MovieTvinfo.Imdb).GetValueOrDefault();
|
||||||
release.TmdbId = row.MovieTvinfo.Tmdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tmdb, out var tmdbResult) ? tmdbResult : 0;
|
release.TmdbId = row.MovieTvinfo.Tmdb.IsNotNullOrWhiteSpace() && ParseUtil.TryCoerceInt(row.MovieTvinfo.Tmdb, out var tmdbResult) ? tmdbResult : 0;
|
||||||
release.TvdbId = row.MovieTvinfo.Tvdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
|
release.TvdbId = row.MovieTvinfo.Tvdb.IsNotNullOrWhiteSpace() && ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseInfos.Add(release);
|
releaseInfos.Add(release);
|
||||||
}
|
}
|
||||||
|
|
||||||
// order by date
|
|
||||||
return releaseInfos
|
return releaseInfos
|
||||||
.OrderByDescending(o => o.PublishDate)
|
.OrderByDescending(o => o.PublishDate)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// hook to adjust category parsing
|
protected virtual IReadOnlyList<IndexerCategory> ParseCategories(AvistazRelease row)
|
||||||
protected virtual List<IndexerCategory> ParseCategories(AvistazRelease row)
|
|
||||||
{
|
{
|
||||||
var cats = new List<IndexerCategory>();
|
var categories = new List<IndexerCategory>();
|
||||||
var resolution = row.VideoQuality;
|
var videoQuality = row.VideoQuality;
|
||||||
|
|
||||||
switch (row.Type)
|
switch (row.Type.ToUpperInvariant())
|
||||||
{
|
{
|
||||||
case "Movie":
|
case "MOVIE":
|
||||||
cats.Add(resolution switch
|
categories.Add(videoQuality switch
|
||||||
{
|
{
|
||||||
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD,
|
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.MoviesHD,
|
||||||
"2160p" => NewznabStandardCategory.MoviesUHD,
|
"2160p" => NewznabStandardCategory.MoviesUHD,
|
||||||
_ => NewznabStandardCategory.MoviesSD
|
_ => NewznabStandardCategory.MoviesSD
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "TV-Show":
|
case "TV-SHOW":
|
||||||
cats.Add(resolution switch
|
categories.Add(videoQuality switch
|
||||||
{
|
{
|
||||||
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.TVHD,
|
var res when _hdResolutions.Contains(res) => NewznabStandardCategory.TVHD,
|
||||||
"2160p" => NewznabStandardCategory.TVUHD,
|
"2160p" => NewznabStandardCategory.TVUHD,
|
||||||
_ => NewznabStandardCategory.TVSD
|
_ => NewznabStandardCategory.TVSD
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "Music":
|
case "MUSIC":
|
||||||
cats.Add(NewznabStandardCategory.Audio);
|
categories.Add(NewznabStandardCategory.Audio);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception($"Error parsing Avistaz category type {row.Type}");
|
throw new Exception($"Error parsing Avistaz category type \"{row.Type}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
return cats;
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual string ParseTitle(AvistazRelease row)
|
||||||
|
{
|
||||||
|
return row.FileName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,11 +82,10 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString();
|
var searchUrl = SearchUrl + "?" + searchParameters.GetQueryString();
|
||||||
|
|
||||||
// TODO: Change to HttpAccept.Json after they fix the issue with missing headers
|
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
||||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
request.HttpRequest.Headers.Set("Authorization", $"Bearer {Settings.Token}");
|
||||||
request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}");
|
|
||||||
|
|
||||||
request.HttpRequest.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
|
request.HttpRequest.SuppressHttpErrorStatusCodes = [HttpStatusCode.NotFound];
|
||||||
|
|
||||||
yield return request;
|
yield return request;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
|||||||
{
|
{
|
||||||
private static readonly AvistazSettingsValidator Validator = new();
|
private static readonly AvistazSettingsValidator Validator = new();
|
||||||
|
|
||||||
public AvistazSettings()
|
public string Token { get; set; } = string.Empty;
|
||||||
{
|
|
||||||
Token = "";
|
|
||||||
FreeleechOnly = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Token { get; set; }
|
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)]
|
[FieldDefinition(2, Label = "Username", HelpText = "IndexerAvistazSettingsUsernameHelpText", HelpTextWarning = "IndexerAvistazSettingsUsernameHelpTextWarning", Privacy = PrivacyLevel.UserName)]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
@@ -112,25 +111,26 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
|
||||||
var queryParams = new NebulanceQuery
|
var queryParams = new NameValueCollection
|
||||||
{
|
{
|
||||||
Age = ">0"
|
{ "action", "search" },
|
||||||
|
{ "age", ">0" },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (searchCriteria.TvMazeId is > 0)
|
if (searchCriteria.TvMazeId is > 0)
|
||||||
{
|
{
|
||||||
queryParams.TvMaze = searchCriteria.TvMazeId.Value;
|
queryParams.Set("tvmaze", searchCriteria.TvMazeId.ToString());
|
||||||
}
|
}
|
||||||
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
else if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
queryParams.Imdb = searchCriteria.FullImdbId;
|
queryParams.Set("imdb", searchCriteria.FullImdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||||
|
|
||||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
queryParams.Release = searchQuery;
|
queryParams.Set("release", searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchCriteria.Season.HasValue &&
|
if (searchCriteria.Season.HasValue &&
|
||||||
@@ -139,43 +139,43 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
queryParams.Name = searchQuery;
|
queryParams.Set("name", searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
queryParams.Release = showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture);
|
queryParams.Set("release", showDate.ToString("yyyy.MM.dd", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (searchCriteria.Season.HasValue)
|
if (searchCriteria.Season.HasValue)
|
||||||
{
|
{
|
||||||
queryParams.Season = searchCriteria.Season.Value;
|
queryParams.Set("season", searchCriteria.Season.Value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber))
|
if (searchCriteria.Episode.IsNotNullOrWhiteSpace() && int.TryParse(searchCriteria.Episode, out var episodeNumber))
|
||||||
{
|
{
|
||||||
queryParams.Episode = episodeNumber;
|
queryParams.Set("episode", episodeNumber.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((queryParams.Season.HasValue || queryParams.Episode.HasValue) &&
|
if ((queryParams.Get("season").IsNotNullOrWhiteSpace() || queryParams.Get("episode").IsNotNullOrWhiteSpace()) &&
|
||||||
queryParams.Name.IsNullOrWhiteSpace() &&
|
queryParams.Get("name").IsNullOrWhiteSpace() &&
|
||||||
queryParams.Release.IsNullOrWhiteSpace() &&
|
queryParams.Get("release").IsNullOrWhiteSpace() &&
|
||||||
!queryParams.TvMaze.HasValue &&
|
queryParams.Get("tvmaze").IsNullOrWhiteSpace() &&
|
||||||
queryParams.Imdb.IsNullOrWhiteSpace())
|
queryParams.Get("imdb").IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
_logger.Debug("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
|
_logger.Warn("NBL API does not support season calls without name, series, id, imdb, tvmaze, or time keys.");
|
||||||
|
|
||||||
return new IndexerPageableRequestChain();
|
return new IndexerPageableRequestChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.Name is { Length: > 0 and < 3 } || queryParams.Release is { Length: > 0 and < 3 })
|
if (queryParams.Get("name") is { Length: > 0 and < 3 } || queryParams.Get("release") is { Length: > 0 and < 3 })
|
||||||
{
|
{
|
||||||
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
|
_logger.Warn("NBL API does not support release calls that are 2 characters or fewer.");
|
||||||
|
|
||||||
return new IndexerPageableRequestChain();
|
return new IndexerPageableRequestChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria));
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
@@ -189,40 +189,45 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
var pageableRequests = new IndexerPageableRequestChain();
|
var pageableRequests = new IndexerPageableRequestChain();
|
||||||
|
|
||||||
var queryParams = new NebulanceQuery
|
var queryParams = new NameValueCollection
|
||||||
{
|
{
|
||||||
Age = ">0"
|
{ "action", "search" },
|
||||||
|
{ "age", ">0" },
|
||||||
};
|
};
|
||||||
|
|
||||||
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
var searchQuery = searchCriteria.SanitizedSearchTerm.Trim();
|
||||||
|
|
||||||
if (searchQuery.IsNotNullOrWhiteSpace())
|
if (searchQuery.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
queryParams.Release = searchQuery;
|
queryParams.Set("release", searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.Release is { Length: > 0 and < 3 })
|
if (queryParams.Get("release") is { Length: > 0 and < 3 })
|
||||||
{
|
{
|
||||||
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
|
_logger.Debug("NBL API does not support release calls that are 2 characters or fewer.");
|
||||||
|
|
||||||
return new IndexerPageableRequestChain();
|
return new IndexerPageableRequestChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria.Limit, searchCriteria.Offset));
|
pageableRequests.Add(GetPagedRequests(queryParams, searchCriteria));
|
||||||
|
|
||||||
return pageableRequests;
|
return pageableRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<IndexerRequest> GetPagedRequests(NebulanceQuery parameters, int? results, int? offset)
|
private IEnumerable<IndexerRequest> GetPagedRequests(NameValueCollection parameters, SearchCriteriaBase searchCriteria)
|
||||||
{
|
{
|
||||||
var apiUrl = _settings.BaseUrl + "api.php";
|
parameters.Set("api_key", _settings.ApiKey);
|
||||||
|
parameters.Set("per_page", searchCriteria.Limit.GetValueOrDefault(100).ToString());
|
||||||
|
|
||||||
var builder = new JsonRpcRequestBuilder(apiUrl)
|
if (searchCriteria.Limit > 0 && searchCriteria.Offset > 0)
|
||||||
.Call("getTorrents", _settings.ApiKey, parameters, results ?? 100, offset ?? 0);
|
{
|
||||||
|
var page = searchCriteria.Offset / searchCriteria.Limit;
|
||||||
|
parameters.Set("page", page.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
builder.SuppressHttpError = true;
|
var apiUrl = $"{_settings.BaseUrl}api.php?{parameters.GetQueryString()}";
|
||||||
|
|
||||||
yield return new IndexerRequest(builder.Build());
|
yield return new IndexerRequest(apiUrl, HttpAccept.Json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||||
@@ -244,16 +249,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
|
|
||||||
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
STJson.TryDeserialize<JsonRpcResponse<NebulanceErrorResponse>>(indexerResponse.HttpResponse.Content, out var errorResponse);
|
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request. Check the logs for more information.", indexerResponse.HttpResponse.StatusCode);
|
||||||
|
|
||||||
throw new IndexerException(indexerResponse, "Unexpected response status '{0}' code from indexer request: {1}", indexerResponse.HttpResponse.StatusCode, errorResponse?.Result?.Error?.Message ?? "Check the logs for more information.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonRpcResponse<NebulanceResponse> jsonResponse;
|
NebulanceResponse jsonResponse;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
jsonResponse = STJson.Deserialize<JsonRpcResponse<NebulanceResponse>>(indexerResponse.HttpResponse.Content);
|
jsonResponse = STJson.Deserialize<NebulanceResponse>(indexerResponse.HttpResponse.Content);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -262,19 +265,17 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", ex, response?.Result ?? ex.Message);
|
throw new IndexerException(indexerResponse, "Unexpected response from indexer request: {0}", ex, response?.Result ?? ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonResponse.Error != null || jsonResponse.Result == null)
|
if (jsonResponse.Error != null)
|
||||||
{
|
{
|
||||||
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error);
|
throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error?.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonResponse.Result?.Items == null || jsonResponse.Result.Items.Count == 0)
|
if (jsonResponse.TotalResults == 0 || jsonResponse.Items == null || jsonResponse.Items.Count == 0)
|
||||||
{
|
{
|
||||||
return torrentInfos;
|
return torrentInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows = jsonResponse.Result.Items;
|
foreach (var row in jsonResponse.Items)
|
||||||
|
|
||||||
foreach (var row in rows)
|
|
||||||
{
|
{
|
||||||
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
|
var details = _settings.BaseUrl + "torrents.php?id=" + row.TorrentId;
|
||||||
|
|
||||||
@@ -284,26 +285,30 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
Guid = details,
|
Guid = details,
|
||||||
InfoUrl = details,
|
InfoUrl = details,
|
||||||
DownloadUrl = row.Download,
|
DownloadUrl = row.DownloadLink,
|
||||||
Title = title.Trim(),
|
Title = title.Trim(),
|
||||||
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
Categories = new List<IndexerCategory> { TvCategoryFromQualityParser.ParseTvShowQuality(row.ReleaseTitle) },
|
||||||
Size = ParseUtil.CoerceLong(row.Size),
|
Size = row.Size,
|
||||||
Files = row.FileList.Count(),
|
Files = row.FileList.Count,
|
||||||
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
PublishDate = DateTime.Parse(row.PublishDateUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
|
||||||
Grabs = ParseUtil.CoerceInt(row.Snatch),
|
Grabs = row.Snatch,
|
||||||
Seeders = ParseUtil.CoerceInt(row.Seed),
|
Seeders = row.Seed,
|
||||||
Peers = ParseUtil.CoerceInt(row.Seed) + ParseUtil.CoerceInt(row.Leech),
|
Peers = row.Seed + row.Leech,
|
||||||
Scene = row.Tags?.ContainsIgnoreCase("scene"),
|
Scene = row.Tags?.ContainsIgnoreCase("scene"),
|
||||||
MinimumRatio = 0, // ratioless
|
MinimumRatio = 0, // ratioless
|
||||||
MinimumSeedTime = row.Category.ToLower() == "season" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
MinimumSeedTime = row.Category.ToUpperInvariant() == "SEASON" ? 432000 : 86400, // 120 hours for seasons and 24 hours for episodes
|
||||||
DownloadVolumeFactor = 0, // ratioless tracker
|
DownloadVolumeFactor = 0, // ratioless tracker
|
||||||
UploadVolumeFactor = 1,
|
UploadVolumeFactor = 1,
|
||||||
PosterUrl = row.Banner
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (row.TvMazeId.IsNotNullOrWhiteSpace())
|
if (row.ImdbId.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
release.TvMazeId = ParseUtil.CoerceInt(row.TvMazeId);
|
release.ImdbId = ParseUtil.GetImdbId(row.ImdbId).GetValueOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.TvMazeId is > 0)
|
||||||
|
{
|
||||||
|
release.TvMazeId = row.TvMazeId.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentInfos.Add(release);
|
torrentInfos.Add(release);
|
||||||
@@ -326,100 +331,55 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NebulanceQuery
|
|
||||||
{
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Time { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="age", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Age { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="tvmaze", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public int? TvMaze { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="imdb", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Imdb { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Hash { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string[] Tags { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="name", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="release", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Release { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Category { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public string Series { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="season", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public int? Season { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName="episode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
|
||||||
public int? Episode { get; set; }
|
|
||||||
|
|
||||||
public NebulanceQuery Clone()
|
|
||||||
{
|
|
||||||
return MemberwiseClone() as NebulanceQuery;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NebulanceResponse
|
public class NebulanceResponse
|
||||||
{
|
{
|
||||||
public List<NebulanceTorrent> Items { get; set; }
|
[JsonPropertyName("total_results")]
|
||||||
|
public int TotalResults { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<NebulanceTorrent> Items { get; init; }
|
||||||
|
|
||||||
|
public NebulanceErrorMessage Error { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NebulanceTorrent
|
public class NebulanceTorrent
|
||||||
{
|
{
|
||||||
[JsonPropertyName("rls_name")]
|
[JsonPropertyName("rls_name")]
|
||||||
public string ReleaseTitle { get; set; }
|
public string ReleaseTitle { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("cat")]
|
[JsonPropertyName("cat")]
|
||||||
public string Category { get; set; }
|
public string Category { get; init; }
|
||||||
|
|
||||||
public string Size { get; set; }
|
public long Size { get; init; }
|
||||||
public string Seed { get; set; }
|
public int Seed { get; init; }
|
||||||
public string Leech { get; set; }
|
public int Leech { get; init; }
|
||||||
public string Snatch { get; set; }
|
public int Snatch { get; init; }
|
||||||
public string Download { get; set; }
|
|
||||||
|
[JsonPropertyName("download")]
|
||||||
|
public string DownloadLink { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("file_list")]
|
[JsonPropertyName("file_list")]
|
||||||
public IEnumerable<string> FileList { get; set; } = Array.Empty<string>();
|
public IReadOnlyCollection<string> FileList { get; init; } = [];
|
||||||
|
|
||||||
[JsonPropertyName("group_name")]
|
[JsonPropertyName("group_name")]
|
||||||
public string GroupName { get; set; }
|
public string GroupName { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("series_banner")]
|
|
||||||
public string Banner { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("group_id")]
|
[JsonPropertyName("group_id")]
|
||||||
public string TorrentId { get; set; }
|
public int TorrentId { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("series_id")]
|
[JsonPropertyName("imdb_id")]
|
||||||
public string TvMazeId { get; set; }
|
public string ImdbId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("tvmaze_id")]
|
||||||
|
public int? TvMazeId { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("rls_utc")]
|
[JsonPropertyName("rls_utc")]
|
||||||
public string PublishDateUtc { get; set; }
|
public string PublishDateUtc { get; init; }
|
||||||
|
|
||||||
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
|
public IReadOnlyCollection<string> Tags { get; init; } = [];
|
||||||
}
|
|
||||||
|
|
||||||
public class NebulanceErrorResponse
|
|
||||||
{
|
|
||||||
public NebulanceErrorMessage Error { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NebulanceErrorMessage
|
public class NebulanceErrorMessage
|
||||||
{
|
{
|
||||||
public string Message { get; set; }
|
public string Message { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
public override string Name => "PrivateHD";
|
public override string Name => "PrivateHD";
|
||||||
public override string[] IndexerUrls => new[] { "https://privatehd.to/" };
|
public override string[] IndexerUrls => new[] { "https://privatehd.to/" };
|
||||||
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeTorrents";
|
public override string Description => "PrivateHD (PHD) is a Private Torrent Tracker for HD MOVIES / TV and the sister-site of AvistaZ, CinemaZ, ExoticaZ, and AnimeZ";
|
||||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||||
|
|
||||||
public PrivateHD(IIndexerRepository indexerRepository,
|
public PrivateHD(IIndexerRepository indexerRepository,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
|
|
||||||
public override IParseIndexerResponse GetParser()
|
public override IParseIndexerResponse GetParser()
|
||||||
{
|
{
|
||||||
return new SceneTimeParser(Settings, Capabilities.Categories);
|
return new SceneTimeParser(Settings, Capabilities.Categories, _logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerCapabilities SetCapabilities()
|
private static IndexerCapabilities SetCapabilities()
|
||||||
{
|
{
|
||||||
var caps = new IndexerCapabilities
|
var caps = new IndexerCapabilities
|
||||||
{
|
{
|
||||||
@@ -213,11 +213,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
private readonly SceneTimeSettings _settings;
|
private readonly SceneTimeSettings _settings;
|
||||||
private readonly IndexerCapabilitiesCategories _categories;
|
private readonly IndexerCapabilitiesCategories _categories;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public SceneTimeParser(SceneTimeSettings settings, IndexerCapabilitiesCategories categories)
|
public SceneTimeParser(SceneTimeSettings settings, IndexerCapabilitiesCategories categories, Logger logger)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_categories = categories;
|
_categories = categories;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||||
@@ -227,31 +229,30 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
var parser = new HtmlParser();
|
var parser = new HtmlParser();
|
||||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||||
|
|
||||||
var table = dom.QuerySelector("table.movehere");
|
var table = dom.QuerySelector("table#torrenttable");
|
||||||
if (table == null)
|
if (table == null)
|
||||||
{
|
{
|
||||||
return releaseInfos; // no results
|
_logger.Error("No results, table element is not present in page.");
|
||||||
|
return releaseInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerColumns = table.QuerySelectorAll("tbody > tr > td.cat_Head")
|
var headerColumns = table.QuerySelectorAll("thead > tr > th")
|
||||||
.Select(x => x.GetAttribute("title").IsNotNullOrWhiteSpace() ? x.GetAttribute("title") : x.TextContent)
|
.Select(x => x.GetAttribute("title") ?? x.QuerySelector("a[title]")?.GetAttribute("title") ?? x.TextContent)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
|
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
|
||||||
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
|
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
|
||||||
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
|
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
|
||||||
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeder(s)", StringComparison.OrdinalIgnoreCase));
|
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeders", StringComparison.OrdinalIgnoreCase));
|
||||||
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leecher(s)", StringComparison.OrdinalIgnoreCase));
|
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leechers", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var rows = dom.QuerySelectorAll("tr.browse");
|
var rows = table.QuerySelectorAll("tbody > tr");
|
||||||
|
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
var qDescCol = row.Children[nameIndex];
|
var qDescCol = row.Children[nameIndex];
|
||||||
var qLink = qDescCol.QuerySelector("a");
|
var qLink = qDescCol.QuerySelector("a");
|
||||||
|
var title = qLink.QuerySelector("span.bw-torrent-name").TextContent.Trim();
|
||||||
// Clean up title
|
|
||||||
qLink.QuerySelectorAll("font[color=\"green\"]").ToList().ForEach(e => e.Remove());
|
|
||||||
var title = qLink.TextContent.Trim();
|
|
||||||
|
|
||||||
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
|
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
|
||||||
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
|
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
|
||||||
@@ -276,7 +277,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
Size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent),
|
Size = ParseUtil.GetBytes(row.Children[sizeIndex].TextContent),
|
||||||
Seeders = seeders,
|
Seeders = seeders,
|
||||||
Peers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent.Trim()) + seeders,
|
Peers = ParseUtil.CoerceInt(row.Children[leechersIndex].TextContent.Trim()) + seeders,
|
||||||
DownloadVolumeFactor = row.QuerySelector("font > b:contains(Freeleech)") != null ? 0 : 1,
|
DownloadVolumeFactor = row.QuerySelector("span.tag.free") is not null ? 0 : 1,
|
||||||
UploadVolumeFactor = 1,
|
UploadVolumeFactor = 1,
|
||||||
MinimumRatio = 1,
|
MinimumRatio = 1,
|
||||||
MinimumSeedTime = 259200 // 72 hours
|
MinimumSeedTime = 259200 // 72 hours
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
"Clear": "Wis",
|
"Clear": "Wis",
|
||||||
"ClearHistory": "Geschiedenis verwijderen",
|
"ClearHistory": "Geschiedenis verwijderen",
|
||||||
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van {appName} wilt verwijderen?",
|
"ClearHistoryMessageText": "Weet je zeker dat je alle geschiedenis van {appName} wilt verwijderen?",
|
||||||
"ClientPriority": "Client Prioriteit",
|
"ClientPriority": "Client prioriteit",
|
||||||
"CloneProfile": "Dupliceer Profiel",
|
"CloneProfile": "Dupliceer Profiel",
|
||||||
"Close": "Sluit",
|
"Close": "Sluit",
|
||||||
"CloseCurrentModal": "Sluit Huidig Bericht",
|
"CloseCurrentModal": "Sluit Huidig Bericht",
|
||||||
|
|||||||
@@ -503,7 +503,7 @@
|
|||||||
"ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas",
|
"ApplyTagsHelpTextAdd": "Adicionar: adicione as etiquetas à lista existente de etiquetas",
|
||||||
"Implementation": "Implementação",
|
"Implementation": "Implementação",
|
||||||
"SelectIndexers": "Pesquisar indexadores",
|
"SelectIndexers": "Pesquisar indexadores",
|
||||||
"ApplyTagsHelpTextHowToApplyApplications": "Como aplicar tags ao autor selecionado",
|
"ApplyTagsHelpTextHowToApplyApplications": "Como aplicar etiquetas aos aplicativos selecionados",
|
||||||
"ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados",
|
"ApplyTagsHelpTextHowToApplyIndexers": "Como aplicar etiquetas aos indexadores selecionados",
|
||||||
"ApplyTagsHelpTextRemove": "Remover: remove as etiquetas inseridas",
|
"ApplyTagsHelpTextRemove": "Remover: remove as etiquetas inseridas",
|
||||||
"ApplyTagsHelpTextReplace": "Substituir: substitui as etiquetas atuais pelas inseridas (deixe em branco para limpar todas as etiquetas)",
|
"ApplyTagsHelpTextReplace": "Substituir: substitui as etiquetas atuais pelas inseridas (deixe em branco para limpar todas as etiquetas)",
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
"AppUpdated": "{appName} atualizado",
|
"AppUpdated": "{appName} atualizado",
|
||||||
"AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName}",
|
"AppUpdatedVersion": "O {appName} foi atualizado para a versão `{version}`. Para obter as alterações mais recentes, recarregue o {appName}",
|
||||||
"ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.",
|
"ConnectionLostToBackend": "O {appName} perdeu a conexão com o backend e precisará ser recarregado para restaurar a funcionalidade.",
|
||||||
"RecentChanges": "Mudanças Recentes",
|
"RecentChanges": "Mudanças recentes",
|
||||||
"WhatsNew": "O que há de novo?",
|
"WhatsNew": "O que há de novo?",
|
||||||
"ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.",
|
"ConnectionLostReconnect": "O {appName} tentará se conectar automaticamente ou você pode clicar em Recarregar abaixo.",
|
||||||
"AddApplicationImplementation": "Adicionar Aplicativo - {implementationName}",
|
"AddApplicationImplementation": "Adicionar Aplicativo - {implementationName}",
|
||||||
@@ -587,7 +587,7 @@
|
|||||||
"DisabledForLocalAddresses": "Desabilitado para endereços locais",
|
"DisabledForLocalAddresses": "Desabilitado para endereços locais",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"None": "Nenhum",
|
"None": "Nenhum",
|
||||||
"ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave de API?",
|
"ResetAPIKeyMessageText": "Tem certeza de que deseja redefinir sua chave da API?",
|
||||||
"AuthBasic": "Básico (pop-up do navegador)",
|
"AuthBasic": "Básico (pop-up do navegador)",
|
||||||
"ActiveIndexers": "Indexadores Ativos",
|
"ActiveIndexers": "Indexadores Ativos",
|
||||||
"ActiveApps": "Apps Ativos",
|
"ActiveApps": "Apps Ativos",
|
||||||
@@ -631,7 +631,7 @@
|
|||||||
"IndexerGazelleGamesSettingsSearchGroupNames": "Pesquisar Nomes de Grupos",
|
"IndexerGazelleGamesSettingsSearchGroupNames": "Pesquisar Nomes de Grupos",
|
||||||
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Pesquisar lançamentos por nomes de grupos",
|
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "Pesquisar lançamentos por nomes de grupos",
|
||||||
"IndexerHDBitsSettingsCodecs": "Codecs",
|
"IndexerHDBitsSettingsCodecs": "Codecs",
|
||||||
"IndexerHDBitsSettingsMediumsHelpText": "se não for especificado, todas as opções serão usadas.",
|
"IndexerHDBitsSettingsMediumsHelpText": "Se não for especificado, todas as opções serão usadas.",
|
||||||
"IndexerHDBitsSettingsOriginsHelpText": "Se não for especificado, todas as opções serão usadas.",
|
"IndexerHDBitsSettingsOriginsHelpText": "Se não for especificado, todas as opções serão usadas.",
|
||||||
"IndexerHDBitsSettingsUseFilenames": "Usar nomes de arquivos",
|
"IndexerHDBitsSettingsUseFilenames": "Usar nomes de arquivos",
|
||||||
"IndexerHDBitsSettingsUsernameHelpText": "Nome de Usuário do Site",
|
"IndexerHDBitsSettingsUsernameHelpText": "Nome de Usuário do Site",
|
||||||
@@ -643,10 +643,10 @@
|
|||||||
"IndexerNzbIndexSettingsApiKeyHelpText": "Chave de API do site",
|
"IndexerNzbIndexSettingsApiKeyHelpText": "Chave de API do site",
|
||||||
"IndexerOrpheusSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
|
"IndexerOrpheusSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
|
||||||
"IndexerPassThePopcornSettingsApiKeyHelpText": "Chave de API do site",
|
"IndexerPassThePopcornSettingsApiKeyHelpText": "Chave de API do site",
|
||||||
"IndexerPassThePopcornSettingsApiUserHelpText": "Essas configurações são encontradas nas configurações de segurança do PassThePopcorn (Editar Perfil > Segurança).",
|
"IndexerPassThePopcornSettingsApiUserHelpText": "Essas configurações estão nas configurações de segurança do PassThePopcorn (Edit Profile [Editar perfil] > Security [Segurança]).",
|
||||||
"IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
|
"IndexerPassThePopcornSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
|
||||||
"IndexerRedactedSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
|
"IndexerRedactedSettingsApiKeyHelpText": "Chave API do site (encontrada em Configurações = Configurações de acesso)",
|
||||||
"IndexerSettingsAdditionalParameters": "Parâmetros Adicionais",
|
"IndexerSettingsAdditionalParameters": "Parâmetros adicionais",
|
||||||
"IndexerSettingsApiPath": "Caminho da API",
|
"IndexerSettingsApiPath": "Caminho da API",
|
||||||
"IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}",
|
"IndexerSettingsApiPathHelpText": "Caminho para a API, geralmente {url}",
|
||||||
"IndexerSettingsApiUser": "Usuário da API",
|
"IndexerSettingsApiUser": "Usuário da API",
|
||||||
@@ -669,7 +669,7 @@
|
|||||||
"IndexerAlphaRatioSettingsExcludeScene": "Excluir SCENE",
|
"IndexerAlphaRatioSettingsExcludeScene": "Excluir SCENE",
|
||||||
"IndexerBeyondHDSettingsApiKeyHelpText": "Chave de API do site (encontrada em Minha segurança = chave de API)",
|
"IndexerBeyondHDSettingsApiKeyHelpText": "Chave de API do site (encontrada em Minha segurança = chave de API)",
|
||||||
"IndexerBeyondHDSettingsSearchTypesHelpText": "Selecione os tipos de lançamentos nos quais você está interessado. Se nenhum for selecionado, todas as opções serão usadas.",
|
"IndexerBeyondHDSettingsSearchTypesHelpText": "Selecione os tipos de lançamentos nos quais você está interessado. Se nenhum for selecionado, todas as opções serão usadas.",
|
||||||
"IndexerHDBitsSettingsCodecsHelpText": "se não for especificado, todas as opções serão usadas.",
|
"IndexerHDBitsSettingsCodecsHelpText": "Se não for especificado, todas as opções serão usadas.",
|
||||||
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostrar apenas lançamentos freeleech",
|
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "Mostrar apenas lançamentos freeleech",
|
||||||
"IndexerHDBitsSettingsUseFilenamesHelpText": "Marque esta opção se quiser usar nomes de arquivos torrent como títulos de lançamento",
|
"IndexerHDBitsSettingsUseFilenamesHelpText": "Marque esta opção se quiser usar nomes de arquivos torrent como títulos de lançamento",
|
||||||
"IndexerIPTorrentsSettingsCookieUserAgent": "Agente de Usuário para Cookies",
|
"IndexerIPTorrentsSettingsCookieUserAgent": "Agente de Usuário para Cookies",
|
||||||
@@ -763,17 +763,17 @@
|
|||||||
"SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download",
|
"SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download",
|
||||||
"Any": "Quaisquer",
|
"Any": "Quaisquer",
|
||||||
"Script": "Script",
|
"Script": "Script",
|
||||||
"BuiltIn": "Embutido",
|
"BuiltIn": "Incorporado",
|
||||||
"InfoUrl": "URL de informações",
|
"InfoUrl": "URL de informações",
|
||||||
"PublishedDate": "Data de Publicação",
|
"PublishedDate": "Data de publicação",
|
||||||
"Redirected": "Redirecionar",
|
"Redirected": "Redirecionado",
|
||||||
"AverageQueries": "Média de Consultas",
|
"AverageQueries": "Média de Consultas",
|
||||||
"AverageGrabs": "Média de Capturas",
|
"AverageGrabs": "Média de Capturas",
|
||||||
"AllSearchResultsHiddenByFilter": "Todos os resultados da pesquisa são ocultados pelo filtro aplicado.",
|
"AllSearchResultsHiddenByFilter": "Todos os resultados da pesquisa são ocultados pelo filtro aplicado.",
|
||||||
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
"PackageVersionInfo": "{packageVersion} por {packageAuthor}",
|
||||||
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
|
"HealthMessagesInfoBox": "Para saber mais sobre a causa dessas mensagens de verificação de integridade, clique no link da wiki (ícone de livro) no final da linha, ou verifique os [logs]({link}). Se tiver dificuldade em interpretar essas mensagens, entre em contato com nosso suporte nos links abaixo.",
|
||||||
"LogSizeLimit": "Limite de Tamanho do Registro",
|
"LogSizeLimit": "Limite de tamanho do log",
|
||||||
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de registro em MB antes do arquivamento. O padrão é 1 MB.",
|
"LogSizeLimitHelpText": "Tamanho máximo do arquivo de log, em MB, antes do arquivamento. O padrão é 1 MB.",
|
||||||
"PreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
"PreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
||||||
"IndexerSettingsPreferMagnetUrl": "Preferir URL Magnético",
|
"IndexerSettingsPreferMagnetUrl": "Preferir URL Magnético",
|
||||||
"IndexerSettingsPreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
"IndexerSettingsPreferMagnetUrlHelpText": "Quando ativado, este indexador preferirá o uso de URLs magnéticos para captura com substituto para links de torrent",
|
||||||
@@ -781,7 +781,7 @@
|
|||||||
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Apenas Golden Popcorn",
|
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "Apenas Golden Popcorn",
|
||||||
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Pesquisar somente lançamentos em Golden Popcorn",
|
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "Pesquisar somente lançamentos em Golden Popcorn",
|
||||||
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
|
"IndexerAvistazSettingsFreeleechOnlyHelpText": "Pesquisar apenas lançamentos freeleech",
|
||||||
"IndexerAvistazSettingsUsernameHelpText": "Nome de Usuário do Site",
|
"IndexerAvistazSettingsUsernameHelpText": "Nome de usuário do site",
|
||||||
"IndexerAvistazSettingsPasswordHelpText": "Senha do Site",
|
"IndexerAvistazSettingsPasswordHelpText": "Senha do Site",
|
||||||
"IndexerAvistazSettingsPidHelpText": "PID da página Minha Conta ou Meu Perfil",
|
"IndexerAvistazSettingsPidHelpText": "PID da página Minha Conta ou Meu Perfil",
|
||||||
"IndexerAvistazSettingsUsernameHelpTextWarning": "Somente membros com rank e acima podem usar a API neste indexador.",
|
"IndexerAvistazSettingsUsernameHelpTextWarning": "Somente membros com rank e acima podem usar a API neste indexador.",
|
||||||
@@ -795,15 +795,15 @@
|
|||||||
"Logout": "Sair",
|
"Logout": "Sair",
|
||||||
"NoEventsFound": "Nenhum evento encontrado",
|
"NoEventsFound": "Nenhum evento encontrado",
|
||||||
"TheLogLevelDefault": "O nível de log padrão é ' Debug ' e pode ser alterado em [ Configurações gerais](/ configurações/geral)",
|
"TheLogLevelDefault": "O nível de log padrão é ' Debug ' e pode ser alterado em [ Configurações gerais](/ configurações/geral)",
|
||||||
"UpdateAppDirectlyLoadError": "Incapaz de atualizar o {appName} diretamente,",
|
"UpdateAppDirectlyLoadError": "Não foi possível atualizar o {appName} diretamente,",
|
||||||
"UpdaterLogFiles": "Arquivos de log do atualizador",
|
"UpdaterLogFiles": "Arquivos de log do atualizador",
|
||||||
"WouldYouLikeToRestoreBackup": "Gostaria de restaurar o backup '{name}'?",
|
"WouldYouLikeToRestoreBackup": "Gostaria de restaurar o backup '{name}'?",
|
||||||
"AptUpdater": "Usar apt para instalar atualizações",
|
"AptUpdater": "Usar apt para instalar atualizações",
|
||||||
"Install": "Instalar",
|
"Install": "Instalar",
|
||||||
"InstallLatest": "Instalar o mais recente",
|
"InstallLatest": "Instalar o mais recente",
|
||||||
"InstallMajorVersionUpdate": "Instalar Atualização",
|
"InstallMajorVersionUpdate": "Instalar atualização",
|
||||||
"InstallMajorVersionUpdateMessage": "Esta atualização instalará uma nova versão principal e pode não ser compatível com o seu sistema. Tem certeza de que deseja instalar esta atualização?",
|
"InstallMajorVersionUpdateMessage": "Esta atualização instalará uma nova versão principal e pode não ser compatível com o seu sistema. Tem certeza de que deseja instalar esta atualização?",
|
||||||
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para obter mais informações.",
|
"InstallMajorVersionUpdateMessageLink": "Verifique [{domain}]({url}) para saber mais.",
|
||||||
"FailedToFetchSettings": "Falha ao obter configurações",
|
"FailedToFetchSettings": "Falha ao obter configurações",
|
||||||
"CurrentlyInstalled": "Atualmente instalado",
|
"CurrentlyInstalled": "Atualmente instalado",
|
||||||
"PreviouslyInstalled": "Instalado anteriormente",
|
"PreviouslyInstalled": "Instalado anteriormente",
|
||||||
|
|||||||
@@ -491,7 +491,7 @@
|
|||||||
"Stats": "Status",
|
"Stats": "Status",
|
||||||
"CurrentlyInstalled": "În prezent instalat",
|
"CurrentlyInstalled": "În prezent instalat",
|
||||||
"Mixed": "Fix",
|
"Mixed": "Fix",
|
||||||
"Season": "Motiv",
|
"Season": "Sezon",
|
||||||
"ActiveIndexers": "Indexatorii activi",
|
"ActiveIndexers": "Indexatorii activi",
|
||||||
"Any": "Oricare",
|
"Any": "Oricare",
|
||||||
"AdvancedSettingsShownClickToHide": "Setări avansate afișate, click pentru a le ascunde",
|
"AdvancedSettingsShownClickToHide": "Setări avansate afișate, click pentru a le ascunde",
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
"GeneralSettings": "通用设置",
|
"GeneralSettings": "通用设置",
|
||||||
"GeneralSettingsSummary": "端口、SSL、用户名/密码、代理、分析、更新",
|
"GeneralSettingsSummary": "端口、SSL、用户名/密码、代理、分析、更新",
|
||||||
"Genre": "类型",
|
"Genre": "类型",
|
||||||
"GrabReleases": "抓取版本",
|
"GrabReleases": "抓取资源",
|
||||||
"GrabTitle": "抓取标题",
|
"GrabTitle": "抓取标题",
|
||||||
"Grabbed": "已抓取",
|
"Grabbed": "已抓取",
|
||||||
"Grabs": "抓取",
|
"Grabs": "抓取",
|
||||||
@@ -621,7 +621,7 @@
|
|||||||
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
||||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)",
|
"DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)",
|
||||||
"DownloadClientQbittorrentSettingsInitialStateHelpText": "添加到 qBittorrent 的种子的初始状态。 请注意,强制做种不遵守种子限制",
|
"DownloadClientQbittorrentSettingsInitialStateHelpText": "添加到 qBittorrent 的种子的初始状态。 请注意,强制做种不遵守种子限制",
|
||||||
"GrabRelease": "抓取版本",
|
"GrabRelease": "抓取资源",
|
||||||
"ManualGrab": "手动抓取",
|
"ManualGrab": "手动抓取",
|
||||||
"OverrideAndAddToDownloadClient": "覆盖并添加到下载队列",
|
"OverrideAndAddToDownloadClient": "覆盖并添加到下载队列",
|
||||||
"OverrideGrabModalTitle": "覆盖并抓取 - {title}",
|
"OverrideGrabModalTitle": "覆盖并抓取 - {title}",
|
||||||
@@ -755,7 +755,7 @@
|
|||||||
"IndexerFileListSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
"IndexerFileListSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
||||||
"IndexerFileListSettingsUsernameHelpText": "网站用户名",
|
"IndexerFileListSettingsUsernameHelpText": "网站用户名",
|
||||||
"IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only",
|
"IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only",
|
||||||
"DownloadClientUTorrentProviderMessage": "由于uTorrent以加密软件、恶意软件和广告而闻名,我们建议切换到更好的客户端,例如qBittorrent、Deluge或ruTorrent。",
|
"DownloadClientUTorrentProviderMessage": "uTorrent 曾经含有挖矿行为、恶意软件和广告,我们强烈建议你选择其他客户端。",
|
||||||
"IndexerId": "索引器",
|
"IndexerId": "索引器",
|
||||||
"IndexerSettingsPasskey": "通行密钥",
|
"IndexerSettingsPasskey": "通行密钥",
|
||||||
"IndexerBeyondHDSettingsRefundOnly": "只读",
|
"IndexerBeyondHDSettingsRefundOnly": "只读",
|
||||||
@@ -763,5 +763,45 @@
|
|||||||
"IndexerBeyondHDSettingsRssKeyHelpText": "来自网站的API密钥(在我的安全 => API密钥)",
|
"IndexerBeyondHDSettingsRssKeyHelpText": "来自网站的API密钥(在我的安全 => API密钥)",
|
||||||
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
||||||
"IndexerHDBitsSettingsOrigins": "原始",
|
"IndexerHDBitsSettingsOrigins": "原始",
|
||||||
"IndexerPassThePopcornSettingsApiUserHelpText": "这些设置位于 PassThePopcorn 安全设置中(编辑个人资料 > 安全,Edit Profile > Security)。"
|
"IndexerPassThePopcornSettingsApiUserHelpText": "这些设置位于 PassThePopcorn 安全设置中(编辑个人资料 > 安全,Edit Profile > Security)。",
|
||||||
|
"IndexerBeyondHDSettingsSearchTypesHelpText": "选择你感兴趣的版本类型。若无则使用所有可选项。",
|
||||||
|
"IndexerFileListSettingsPasskeyHelpText": "网站密钥(下载客户端中跟踪器链接显示的由字母和数字构成的字符串)",
|
||||||
|
"IndexerGazelleGamesSettingsApiKeyHelpText": "来自网站的 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "必须具有用户和种子权限",
|
||||||
|
"IndexerGazelleGamesSettingsSearchGroupNames": "搜索群组名",
|
||||||
|
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "根据群组名搜索版本",
|
||||||
|
"IndexerHDBitsSettingsPasskeyHelpText": "用户详情中的密钥",
|
||||||
|
"IndexerHDBitsSettingsUseFilenames": "使用文件名",
|
||||||
|
"IndexerHDBitsSettingsUseFilenamesHelpText": "勾选此选项如果你想将种子文件名作为发布资源的标题",
|
||||||
|
"IndexerIPTorrentsSettingsCookieUserAgentHelpText": "浏览器中 cookie 所关联的 User-Agent",
|
||||||
|
"IndexerMTeamTpSettingsApiKeyHelpText": "站点中的 API 密钥(在用户控制面板 => 安全 => 实验室中)",
|
||||||
|
"IndexerNebulanceSettingsApiKeyHelpText": "在用户设置 > API 密钥中的API 密钥。密钥必须有列出和下载的权限",
|
||||||
|
"IndexerNewznabSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerSettingsCookieHelpText": "站点 Cookie",
|
||||||
|
"IndexerSettingsFreeleechOnly": "仅免流资源",
|
||||||
|
"IndexerSettingsGrabLimit": "抓取上限",
|
||||||
|
"IndexerSettingsGrabLimitHelpText": "在该时间单位内,{appName} 对此站点所允许的最大抓取数",
|
||||||
|
"IndexerSettingsLimitsUnit": "限制单位",
|
||||||
|
"IndexerSettingsLimitsUnitHelpText": "每个索引器计算上限的时间单位",
|
||||||
|
"IndexerSettingsPreferMagnetUrl": "磁力链接优先",
|
||||||
|
"IndexerSettingsPreferMagnetUrlHelpText": "若开启,该索引器将优先使用磁力链接抓取,而种子链接作为备用",
|
||||||
|
"IndexerSettingsQueryLimit": "请求上限",
|
||||||
|
"IndexerSettingsQueryLimitHelpText": "在该时间单位内,{appName} 对此站点所允许的最大请求数",
|
||||||
|
"IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent",
|
||||||
|
"IndexerNewznabSettingsVipExpirationHelpText": "输入 VIP 到期日期 (yyyy-mm-dd) 或留空,{appName} 将在到期前 1 星期发送通知",
|
||||||
|
"IndexerNzbIndexSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerOrpheusSettingsApiKeyHelpText": "站点 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerPassThePopcornSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "仅 Golden Popcorn",
|
||||||
|
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "仅搜索 Golden Popcorn 发布的资源",
|
||||||
|
"IndexerRedactedSettingsApiKeyHelpText": "站点 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerSettingsBaseUrl": "基础链接",
|
||||||
|
"IndexerSettingsBaseUrlHelpText": "选择 {appName} 请求该站点所使用的基础链接",
|
||||||
|
"IndexerTorrentSyndikatSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"NoApplicationsFound": "找不到程序",
|
||||||
|
"Open": "打开",
|
||||||
|
"PreferMagnetUrl": "磁力链接优先",
|
||||||
|
"PreferMagnetUrlHelpText": "若开启,该索引器将优先使用磁力链接,种子链接作为备用",
|
||||||
|
"ProwlarrDownloadClientsAlert": "如果你想直接在 {appName} 中搜索,你需要添加下载管理器,否则你不需要在此添加。你的程序中的搜索行为将使用它自己的下载管理器配置。",
|
||||||
|
"ProwlarrDownloadClientsInAppOnlyAlert": "下载管理器仅对 {appName} 中搜索有效,而不会同步到其他程序中。目前没有添加此功能的计划。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.6.4" />
|
<PackageReference Include="Polly" Version="8.6.4" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.Core" Version="6.2.0" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="6.2.0" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="6.2.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.19" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.19" />
|
||||||
<PackageReference Include="System.Memory" Version="4.6.3" />
|
<PackageReference Include="System.Memory" Version="4.6.3" />
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="8.0.0" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="8.0.0" />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
|||||||
|
|
||||||
if (behavior != MockBehavior.Default && mock.Behavior == MockBehavior.Default)
|
if (behavior != MockBehavior.Default && mock.Behavior == MockBehavior.Default)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Unable to change be behaviour of a an existing mock.");
|
throw new InvalidOperationException("Unable to change be behaviour of an existing mock.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mock;
|
return mock;
|
||||||
@@ -139,7 +139,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
|||||||
|
|
||||||
LoadPlatformLibrary();
|
LoadPlatformLibrary();
|
||||||
|
|
||||||
AssemblyLoader.RegisterSQLiteResolver();
|
AssemblyLoader.RegisterNativeResolver(new[] { "System.Data.SQLite", "Prowlarr.Core" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
|
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
|
||||||
|
|||||||
Reference in New Issue
Block a user