mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-12 15:49:55 -04:00
Compare commits
20 Commits
instance-n
...
v0.4.0.183
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580fc528e5 | ||
|
|
dfed229a1d | ||
|
|
e76a255229 | ||
|
|
a0b650e7a5 | ||
|
|
7cf9fc6a4f | ||
|
|
86f5768461 | ||
|
|
479e29dde7 | ||
|
|
761e15a476 | ||
|
|
d3ffb7b490 | ||
|
|
0db804b647 | ||
|
|
4334e7eef1 | ||
|
|
fbfb70a1bb | ||
|
|
59e990227d | ||
|
|
f0abfae978 | ||
|
|
40e7f80be9 | ||
|
|
3c913eac24 | ||
|
|
95a2bd3d03 | ||
|
|
d5abde98e1 | ||
|
|
5d14d2c134 | ||
|
|
ed46c5c86d |
@@ -78,7 +78,7 @@
|
||||
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--white);
|
||||
background-color: var(--inputBackgroundColor);
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
@@ -74,7 +74,7 @@ class PageHeader extends Component {
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
to="https://opencollective.com/prowlarr"
|
||||
to="https://prowlarr.com/donate"
|
||||
size={14}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
position: relative;
|
||||
|
||||
&.default {
|
||||
background-color: var(--white);
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
box-shadow: 0 5px 10px var(--popoverShadowColor);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ const requiresRestartKeys = [
|
||||
'bindAddress',
|
||||
'port',
|
||||
'urlBase',
|
||||
'instanceName',
|
||||
'enableSsl',
|
||||
'sslPort',
|
||||
'sslCertPath',
|
||||
|
||||
@@ -168,10 +168,11 @@ module.exports = {
|
||||
//
|
||||
// Popover
|
||||
|
||||
popoverTitleBackgroundColor: '#f7f7f7',
|
||||
popoverTitleBorderColor: '#ebebeb',
|
||||
popoverTitleBackgroundColor: '#424242',
|
||||
popoverTitleBorderColor: '#2a2a2a',
|
||||
popoverBodyBackgroundColor: '#2a2a2a',
|
||||
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
popoverArrowBorderColor: '#fff',
|
||||
popoverArrowBorderColor: '#2a2a2a',
|
||||
|
||||
popoverTitleBackgroundInverseColor: '#595959',
|
||||
popoverTitleBorderInverseColor: '#707070',
|
||||
|
||||
@@ -170,6 +170,7 @@ module.exports = {
|
||||
|
||||
popoverTitleBackgroundColor: '#f7f7f7',
|
||||
popoverTitleBorderColor: '#ebebeb',
|
||||
popoverBodyBackgroundColor: '#e9e9e9',
|
||||
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
popoverArrowBorderColor: '#fff',
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class Donations extends Component {
|
||||
return (
|
||||
<FieldSet legend={translate('Donations')}>
|
||||
<div className={styles.logoContainer} title="Radarr">
|
||||
<Link to="https://opencollective.com/radarr">
|
||||
<Link to="https://radarr.video/donate">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
|
||||
@@ -21,7 +21,7 @@ class Donations extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.logoContainer} title="Lidarr">
|
||||
<Link to="https://opencollective.com/lidarr">
|
||||
<Link to="https://lidarr.audio/donate">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
|
||||
@@ -29,7 +29,7 @@ class Donations extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.logoContainer} title="Readarr">
|
||||
<Link to="https://opencollective.com/readarr">
|
||||
<Link to="https://readarr.com/donate">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
|
||||
@@ -37,7 +37,7 @@ class Donations extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.logoContainer} title="Prowlarr">
|
||||
<Link to="https://opencollective.com/prowlarr">
|
||||
<Link to="https://prowlarr.com/donate">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}
|
||||
|
||||
42
package.json
42
package.json
@@ -78,38 +78,38 @@
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/eslint-parser": "7.17.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-decorators": "7.17.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.16.7",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
|
||||
"@babel/plugin-proposal-function-sent": "7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
|
||||
"@babel/core": "7.18.2",
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.17.12",
|
||||
"@babel/plugin-proposal-decorators": "7.18.2",
|
||||
"@babel/plugin-proposal-export-default-from": "7.17.12",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
|
||||
"@babel/plugin-proposal-function-sent": "7.18.2",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.17.12",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"autoprefixer": "10.4.4",
|
||||
"babel-loader": "8.2.4",
|
||||
"@babel/preset-env": "7.18.2",
|
||||
"@babel/preset-react": "7.17.12",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.21.1",
|
||||
"core-js": "3.22.8",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.11.0",
|
||||
"eslint": "8.17.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-react": "7.30.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "3.3.0",
|
||||
"esprint": "3.6.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "6.1.7",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"loader-utils": "^3.0.0",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"postcss": "8.4.12",
|
||||
"postcss": "8.4.14",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-mixins": "9.0.2",
|
||||
@@ -121,10 +121,10 @@
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "14.6.0",
|
||||
"stylelint": "14.8.5",
|
||||
"stylelint-order": "5.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.70.0",
|
||||
"webpack": "5.73.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-livereload-plugin": "3.0.2"
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
|
||||
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// animebytes response
|
||||
[TestCase(@"""Link"":""https:\/\/animebytes.tv\/torrent\/994064\/download\/tb51db35e1910123321025a12b9933d2"",")]
|
||||
|
||||
// danish bytes response
|
||||
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
|
||||
@@ -77,20 +80,24 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
// Download Station
|
||||
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
|
||||
|
||||
// Tracker Responses
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
|
||||
// BroadcastheNet
|
||||
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
|
||||
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
|
||||
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
|
||||
|
||||
// Notifiarr
|
||||
// Webhooks - Notifiarr
|
||||
[TestCase(@"https://xxx.yyy/api/v1/notification/prowlarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
||||
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
|
||||
// RSS
|
||||
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&extended=1&cat=3030&apikey=mySecret&q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
|
||||
|
||||
// Internal
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
|
||||
public void should_clean_message(string message)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
@@ -28,6 +28,9 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
|
||||
// NzbGet
|
||||
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -52,6 +55,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
|
||||
// Indexer Responses
|
||||
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}\\\/rss\\\/download\\\/(?<secret>[^&=]+?)\\\/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?:animebytes)\.[a-z]{2,3}\\\/torrent\\\/[0-9]+\\\/download\\\/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
5684
src/NzbDrone.Core.Test/Files/Indexers/Exoticaz/recentfeed.json
Normal file
5684
src/NzbDrone.Core.Test/Files/Indexers/Exoticaz/recentfeed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Definitions;
|
||||
using NzbDrone.Core.Indexers.Definitions.Avistaz;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ExoticazFixture : CoreTest<ExoticaZ>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.Definition = new IndexerDefinition()
|
||||
{
|
||||
Name = "ExoticaZ",
|
||||
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_parse_recent_feed_from_ExoticaZ()
|
||||
{
|
||||
var recentFeed = ReadAllText(@"Files/Indexers/Exoticaz/recentfeed.json");
|
||||
|
||||
Mocker.GetMock<IIndexerHttpClient>()
|
||||
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
|
||||
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
|
||||
|
||||
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
|
||||
|
||||
releases.Should().HaveCount(100);
|
||||
releases.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var torrentInfo = releases.First() as TorrentInfo;
|
||||
|
||||
torrentInfo.Title.Should().Be("[SSIS-419] My first experience is Yua Mikami. From the day I lost my virginity, I was devoted to sex.");
|
||||
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
torrentInfo.DownloadUrl.Should().Be("https://exoticaz.to/rss/download/(removed)/(removed).torrent");
|
||||
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
|
||||
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
|
||||
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
|
||||
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
|
||||
torrentInfo.Size.Should().Be(7085405541);
|
||||
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
|
||||
torrentInfo.MagnetUrl.Should().Be(null);
|
||||
torrentInfo.Peers.Should().Be(33);
|
||||
torrentInfo.Seeders.Should().Be(33);
|
||||
torrentInfo.Categories.First().Id.Should().Be(6040);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
|
||||
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
|
||||
|
||||
torrents.Should().HaveCount(293);
|
||||
torrents.First().Should().BeOfType<PassThePopcornInfo>();
|
||||
torrents.First().Should().BeOfType<TorrentInfo>();
|
||||
|
||||
var first = torrents.First() as TorrentInfo;
|
||||
|
||||
|
||||
@@ -23,11 +23,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var enabled = _indexerFactory.Enabled(false);
|
||||
var indexers = _indexerFactory.AllProviders(false);
|
||||
var expiringProviders = new List<IIndexer>();
|
||||
var expiredProviders = new List<IIndexer>();
|
||||
|
||||
foreach (var provider in enabled)
|
||||
foreach (var provider in indexers)
|
||||
{
|
||||
var settingsType = provider.Definition.Settings.GetType();
|
||||
var vipProp = settingsType.GetProperty("VipExpiration");
|
||||
@@ -44,11 +43,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DateTime.Parse(expiration).Before(DateTime.Now))
|
||||
{
|
||||
expiredProviders.Add(provider);
|
||||
}
|
||||
|
||||
if (DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7)))
|
||||
{
|
||||
expiringProviders.Add(provider);
|
||||
@@ -64,15 +58,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
|
||||
"#indexer-vip-expiring");
|
||||
}
|
||||
|
||||
if (!expiredProviders.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Warning,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"),
|
||||
string.Join(", ", expiredProviders.Select(v => v.Definition.Name))),
|
||||
"#indexer-vip-expired");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.ThingiProvider.Events;
|
||||
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
|
||||
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
|
||||
public class IndexerVIPExpiredCheck : HealthCheckBase
|
||||
{
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public IndexerVIPExpiredCheck(IIndexerFactory indexerFactory, ILocalizationService localizationService)
|
||||
: base(localizationService)
|
||||
{
|
||||
_indexerFactory = indexerFactory;
|
||||
}
|
||||
|
||||
public override HealthCheck Check()
|
||||
{
|
||||
var indexers = _indexerFactory.AllProviders(false);
|
||||
var expiredProviders = new List<IIndexer>();
|
||||
|
||||
foreach (var provider in indexers)
|
||||
{
|
||||
var settingsType = provider.Definition.Settings.GetType();
|
||||
var vipProp = settingsType.GetProperty("VipExpiration");
|
||||
|
||||
if (vipProp == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var expiration = (string)vipProp.GetValue(provider.Definition.Settings);
|
||||
|
||||
if (expiration.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DateTime.Parse(expiration).Before(DateTime.Now))
|
||||
{
|
||||
expiredProviders.Add(provider);
|
||||
}
|
||||
}
|
||||
|
||||
if (!expiredProviders.Empty())
|
||||
{
|
||||
return new HealthCheck(GetType(),
|
||||
HealthCheckResult.Error,
|
||||
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"),
|
||||
string.Join(", ", expiredProviders.Select(v => v.Definition.Name))),
|
||||
"#indexer-vip-expired");
|
||||
}
|
||||
|
||||
return new HealthCheck(GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
let t = (r as TorrentInfo) ?? new TorrentInfo()
|
||||
select new XElement("item",
|
||||
new XElement("title", RemoveInvalidXMLChars(r.Title)),
|
||||
new XElement("description", RemoveInvalidXMLChars(r.Description)),
|
||||
new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory
|
||||
new XElement("prowlarrindexer", new XAttribute("id", r.IndexerId), r.Indexer),
|
||||
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Download { get; set; }
|
||||
public Dictionary<string, string> Category { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "movie_tv")]
|
||||
public AvistazIdInfo MovieTvinfo { get; set; }
|
||||
|
||||
@@ -201,6 +201,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerHttpResponse);
|
||||
|
||||
if (jsonResponse.Resource.StatusCode == 0)
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Indexer Error: {jsonResponse.Resource.StatusMessage}");
|
||||
}
|
||||
|
||||
foreach (var row in jsonResponse.Resource.Results)
|
||||
{
|
||||
var details = row.InfoUrl;
|
||||
@@ -272,6 +277,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public class BeyondHDResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "status_code")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "status_message")]
|
||||
public string StatusMessage { get; set; }
|
||||
public List<BeyondHDTorrent> Results { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
|
||||
protected virtual string SiteLink { get; private set; }
|
||||
|
||||
protected readonly List<CategoryMapping> _categoryMapping = new List<CategoryMapping>();
|
||||
protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories();
|
||||
protected readonly List<string> _defaultCategories = new List<string>();
|
||||
|
||||
protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "rageid", "tmdbid", "tvdbid", "poster", "banner", "description" };
|
||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
continue;
|
||||
}
|
||||
|
||||
AddCategoryMapping(category.Key, cat);
|
||||
_categories.AddCategoryMapping(category.Key, cat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
|
||||
_categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
|
||||
|
||||
if (categorymapping.Default)
|
||||
{
|
||||
@@ -105,30 +105,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCategoryMapping(string trackerCategory, IndexerCategory torznabCategory, string trackerCategoryDesc = null)
|
||||
{
|
||||
_categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, torznabCategory.Id));
|
||||
|
||||
if (trackerCategoryDesc == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// create custom cats (1:1 categories) if trackerCategoryDesc is defined
|
||||
// - if trackerCategory is "integer" we use that number to generate custom category id
|
||||
// - if trackerCategory is "string" we compute a hash to generate fixed integer id for the custom category
|
||||
// the hash is not perfect but it should work in most cases. we can't use sequential numbers because
|
||||
// categories are updated frequently and the id must be fixed to work in 3rd party apps
|
||||
if (!int.TryParse(trackerCategory, out var trackerCategoryInt))
|
||||
{
|
||||
var hashed = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(trackerCategory));
|
||||
trackerCategoryInt = BitConverter.ToUInt16(hashed, 0); // id between 0 and 65535 < 100000
|
||||
}
|
||||
|
||||
var customCat = new IndexerCategory(trackerCategoryInt + 100000, trackerCategoryDesc);
|
||||
_categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, customCat.Id));
|
||||
}
|
||||
|
||||
protected IElement QuerySelector(IElement element, string selector)
|
||||
{
|
||||
// AngleSharp doesn't support the :root pseudo selector, so we check for it manually
|
||||
@@ -362,54 +338,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
return variables;
|
||||
}
|
||||
|
||||
protected ICollection<IndexerCategory> MapTrackerCatToNewznab(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return new List<IndexerCategory>();
|
||||
}
|
||||
|
||||
var cats = _categoryMapping
|
||||
.Where(m =>
|
||||
!string.IsNullOrWhiteSpace(m.TrackerCategory) &&
|
||||
string.Equals(m.TrackerCategory, input, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
|
||||
.ToList();
|
||||
return cats;
|
||||
}
|
||||
|
||||
public List<string> MapTorznabCapsToTrackers(int[] searchCategories, bool mapChildrenCatsToParent = false)
|
||||
{
|
||||
if (searchCategories == null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
results.AddRange(_categoryMapping
|
||||
.Where(c => searchCategories.Contains(c.NewzNabCategory))
|
||||
.Select(mapping => mapping.TrackerCategory).Distinct().ToList());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public ICollection<IndexerCategory> MapTrackerCatDescToNewznab(string trackerCategoryDesc)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(trackerCategoryDesc))
|
||||
{
|
||||
return new List<IndexerCategory>();
|
||||
}
|
||||
|
||||
var cats = _categoryMapping
|
||||
.Where(m =>
|
||||
!string.IsNullOrWhiteSpace(m.TrackerCategoryDesc) &&
|
||||
string.Equals(m.TrackerCategoryDesc, trackerCategoryDesc, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
|
||||
.ToList();
|
||||
return cats;
|
||||
}
|
||||
|
||||
protected delegate string TemplateTextModifier(string str);
|
||||
|
||||
protected string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
|
||||
|
||||
@@ -451,7 +451,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
value = release.Description;
|
||||
break;
|
||||
case "category":
|
||||
var cats = MapTrackerCatToNewznab(value);
|
||||
var cats = _categories.MapTrackerCatToNewznab(value);
|
||||
if (cats.Any())
|
||||
{
|
||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||
@@ -467,7 +467,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
value = release.Categories.ToString();
|
||||
break;
|
||||
case "categorydesc":
|
||||
var catsDesc = MapTrackerCatDescToNewznab(value);
|
||||
var catsDesc = _categories.MapTrackerCatDescToNewznab(value);
|
||||
if (catsDesc.Any())
|
||||
{
|
||||
if (release.Categories == null || fieldModifiers.Contains("noappend"))
|
||||
|
||||
@@ -975,7 +975,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
{
|
||||
var search = _definition.Search;
|
||||
|
||||
var mappedCategories = MapTorznabCapsToTrackers((int[])variables[".Query.Categories"]);
|
||||
var mappedCategories = _categories.MapTorznabCapsToTrackers((int[])variables[".Query.Categories"]);
|
||||
if (mappedCategories.Count == 0)
|
||||
{
|
||||
mappedCategories = _defaultCategories;
|
||||
@@ -1000,7 +1000,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
||||
}
|
||||
|
||||
variables[".Query.Keywords"] = string.Join(" ", keywordTokens);
|
||||
variables[".Keywords"] = ApplyFilters((string)variables[".Query.Keywords"], search.Keywordsfilters);
|
||||
variables[".Keywords"] = ApplyFilters((string)variables[".Query.Keywords"], search.Keywordsfilters, variables);
|
||||
|
||||
// TODO: prepare queries first and then send them parallel
|
||||
var searchPaths = search.Paths;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Definitions.Avistaz;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -30,15 +30,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
};
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new ExoticaZParser(Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
var caps = new IndexerCapabilities();
|
||||
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.XXXx264, "Video Clip");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.XXXPack, "Video Pack");
|
||||
@@ -52,4 +51,21 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExoticaZParser : AvistazParser
|
||||
{
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public ExoticaZParser(IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
protected override List<IndexerCategory> ParseCategories(AvistazRelease row)
|
||||
{
|
||||
var cat = row.Category;
|
||||
|
||||
return cat.SelectMany(c => _categories.MapTrackerCatToNewznab(c.Key)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.FileList
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
public int TotalSeeders { get; set; }
|
||||
public int TotalSnatched { get; set; }
|
||||
public long MaxSize { get; set; }
|
||||
public long GroupTime { get; set; }
|
||||
public string GroupTime { get; set; }
|
||||
public List<GazelleTorrent> Torrents { get; set; }
|
||||
public bool IsFreeLeech { get; set; }
|
||||
public bool IsNeutralLeech { get; set; }
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Net;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Gazelle
|
||||
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
|
||||
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
|
||||
PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime((string)result.GroupTime),
|
||||
PosterUrl = posterUrl,
|
||||
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
|
||||
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
get
|
||||
{
|
||||
yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"));
|
||||
yield return GetDefinition("altHUB", GetSettings("https://althub.co.za"));
|
||||
yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"));
|
||||
yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"));
|
||||
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
|
||||
yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"));
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
{
|
||||
public class PassThePopcornInfo : TorrentInfo
|
||||
{
|
||||
public bool? Golden { get; set; }
|
||||
public bool? Scene { get; set; }
|
||||
public bool? Approved { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
// Only add approved torrents
|
||||
try
|
||||
{
|
||||
torrentInfos.Add(new PassThePopcornInfo()
|
||||
torrentInfos.Add(new TorrentInfo()
|
||||
{
|
||||
Guid = string.Format("PassThePopcorn-{0}", id),
|
||||
Title = title,
|
||||
@@ -104,9 +104,6 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
Seeders = int.Parse(torrent.Seeders),
|
||||
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
|
||||
PublishDate = torrent.UploadTime.ToUniversalTime(),
|
||||
Golden = torrent.GoldenPopcorn,
|
||||
Scene = torrent.Scene,
|
||||
Approved = torrent.Checked,
|
||||
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
|
||||
IndexerFlags = flags,
|
||||
MinimumRatio = 1,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
{
|
||||
@@ -37,9 +39,21 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
|
||||
{
|
||||
var queryParams = new NameValueCollection
|
||||
{
|
||||
{ "action", "advanced" },
|
||||
{ "json", "noredirect" },
|
||||
{ "searchstr", searchParameters }
|
||||
};
|
||||
|
||||
if (Settings.FreeleechOnly)
|
||||
{
|
||||
queryParams.Add("freetorrent", "1");
|
||||
}
|
||||
|
||||
var request =
|
||||
new IndexerRequest(
|
||||
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?action=advanced&json=noredirect&searchstr={searchParameters}",
|
||||
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{queryParams.GetQueryString()}",
|
||||
HttpAccept.Json);
|
||||
|
||||
request.HttpRequest.Headers["ApiUser"] = Settings.APIUser;
|
||||
|
||||
@@ -22,12 +22,15 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
|
||||
{
|
||||
}
|
||||
|
||||
[FieldDefinition(2, Label = "APIUser", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(2, Label = "API User", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
|
||||
public string APIUser { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string APIKey { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Freeleech Only", HelpText = "Return only freeleech torrents", Type = FieldType.Checkbox)]
|
||||
public bool FreeleechOnly { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -16,6 +16,7 @@ using NzbDrone.Core.Indexers.Gazelle;
|
||||
using NzbDrone.Core.Indexers.Settings;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
@@ -276,7 +277,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
InfoUrl = infoUrl,
|
||||
Seeders = int.Parse(result.Seeders),
|
||||
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
|
||||
Freeleech = result.IsFreeLeech || result.IsPersonalFreeLeech,
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
|
||||
@@ -17,6 +17,7 @@ using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Indexers.Gazelle;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
@@ -182,7 +183,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
|
||||
Files = result.FileCount,
|
||||
Grabs = result.Snatches,
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
|
||||
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
|
||||
};
|
||||
|
||||
var category = result.Category;
|
||||
|
||||
@@ -62,10 +62,10 @@ namespace NzbDrone.Core.Validation
|
||||
return ruleBuilder.WithState(v => NzbDroneValidationState.Warning);
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, string> StartsOrEndsWithProwlarr<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
public static IRuleBuilderOptions<T, string> ContainsProwlarr<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
return ruleBuilder.SetValidator(new RegularExpressionValidator("^Prowlarr|Prowlarr$")).WithMessage("Must start or end with Prowlarr");
|
||||
return ruleBuilder.SetValidator(new RegularExpressionValidator("prowlarr", RegexOptions.IgnoreCase)).WithMessage("Must contain Prowlarr");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr18" />
|
||||
|
||||
@@ -3,17 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DryIoc;
|
||||
using Moq;
|
||||
using Moq.Language.Flow;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Test.Common.AutoMoq.Unity;
|
||||
using Unity;
|
||||
|
||||
[assembly: InternalsVisibleTo("AutoMoq.Tests")]
|
||||
|
||||
namespace NzbDrone.Test.Common.AutoMoq
|
||||
{
|
||||
@@ -21,32 +15,18 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
public class AutoMoqer
|
||||
{
|
||||
public readonly MockBehavior DefaultBehavior = MockBehavior.Default;
|
||||
public Type ResolveType;
|
||||
private IUnityContainer _container;
|
||||
private IContainer _container;
|
||||
private IDictionary<Type, object> _registeredMocks;
|
||||
|
||||
public AutoMoqer()
|
||||
{
|
||||
SetupAutoMoqer(new UnityContainer());
|
||||
}
|
||||
|
||||
public AutoMoqer(MockBehavior defaultBehavior)
|
||||
{
|
||||
DefaultBehavior = defaultBehavior;
|
||||
SetupAutoMoqer(new UnityContainer());
|
||||
}
|
||||
|
||||
public AutoMoqer(IUnityContainer container)
|
||||
{
|
||||
SetupAutoMoqer(container);
|
||||
SetupAutoMoqer(CreateTestContainer(new Container()));
|
||||
}
|
||||
|
||||
public virtual T Resolve<T>()
|
||||
{
|
||||
ResolveType = typeof(T);
|
||||
var result = _container.Resolve<T>();
|
||||
SetConstant(result);
|
||||
ResolveType = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -59,7 +39,6 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
public virtual Mock<T> GetMock<T>(MockBehavior behavior)
|
||||
where T : class
|
||||
{
|
||||
ResolveType = null;
|
||||
var type = GetTheMockType<T>();
|
||||
if (GetMockHasNotBeenCalledForThisType(type))
|
||||
{
|
||||
@@ -78,87 +57,81 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
|
||||
public virtual void SetMock(Type type, Mock mock)
|
||||
{
|
||||
if (_registeredMocks.ContainsKey(type) == false)
|
||||
if (GetMockHasNotBeenCalledForThisType(type))
|
||||
{
|
||||
_registeredMocks.Add(type, mock);
|
||||
}
|
||||
|
||||
if (mock != null)
|
||||
{
|
||||
_container.RegisterInstance(type, mock.Object);
|
||||
_container.RegisterInstance(type, mock.Object, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetConstant<T>(T instance)
|
||||
{
|
||||
_container.RegisterInstance(instance);
|
||||
_container.RegisterInstance(instance, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
||||
SetMock(instance.GetType(), null);
|
||||
}
|
||||
|
||||
public ISetup<T> Setup<T>(Expression<Action<T>> expression)
|
||||
where T : class
|
||||
private IContainer CreateTestContainer(IContainer container)
|
||||
{
|
||||
return GetMock<T>().Setup(expression);
|
||||
var c = container.CreateChild(IfAlreadyRegistered.Replace,
|
||||
container.Rules
|
||||
.WithDynamicRegistration((serviceType, serviceKey) =>
|
||||
{
|
||||
// ignore services with non-default key
|
||||
if (serviceKey != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(object))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serviceType.IsGenericType && serviceType.IsOpenGeneric())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the Mock object for the abstract class or interface
|
||||
if (serviceType.IsInterface || serviceType.IsAbstract)
|
||||
{
|
||||
var mockType = typeof(Mock<>).MakeGenericType(serviceType);
|
||||
var mockFactory = new DelegateFactory(r =>
|
||||
{
|
||||
var mock = (Mock)r.Resolve(mockType);
|
||||
SetMock(serviceType, mock);
|
||||
return mock.Object;
|
||||
}, Reuse.Singleton);
|
||||
|
||||
return new[] { new DynamicRegistration(mockFactory, IfAlreadyRegistered.Keep) };
|
||||
}
|
||||
|
||||
// concrete types
|
||||
var concreteTypeFactory = serviceType.ToFactory(Reuse.Singleton, FactoryMethod.ConstructorWithResolvableArgumentsIncludingNonPublic);
|
||||
|
||||
return new[] { new DynamicRegistration(concreteTypeFactory) };
|
||||
},
|
||||
DynamicRegistrationFlags.Service | DynamicRegistrationFlags.AsFallback));
|
||||
|
||||
c.Register(typeof(Mock<>), Reuse.Singleton, FactoryMethod.DefaultConstructor());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
public ISetup<T, TResult> Setup<T, TResult>(Expression<Func<T, TResult>> expression)
|
||||
where T : class
|
||||
{
|
||||
return GetMock<T>().Setup(expression);
|
||||
}
|
||||
|
||||
public void Verify<T>(Expression<Action<T>> expression)
|
||||
where T : class
|
||||
{
|
||||
GetMock<T>().Verify(expression);
|
||||
}
|
||||
|
||||
public void Verify<T>(Expression<Action<T>> expression, string failMessage)
|
||||
where T : class
|
||||
{
|
||||
GetMock<T>().Verify(expression, failMessage);
|
||||
}
|
||||
|
||||
public void Verify<T>(Expression<Action<T>> expression, Times times)
|
||||
where T : class
|
||||
{
|
||||
GetMock<T>().Verify(expression, times);
|
||||
}
|
||||
|
||||
public void Verify<T>(Expression<Action<T>> expression, Times times, string failMessage)
|
||||
where T : class
|
||||
{
|
||||
GetMock<T>().Verify(expression, times, failMessage);
|
||||
}
|
||||
|
||||
public void VerifyAllMocks()
|
||||
{
|
||||
foreach (var registeredMock in _registeredMocks)
|
||||
{
|
||||
var mock = registeredMock.Value as Mock;
|
||||
if (mock != null)
|
||||
{
|
||||
mock.VerifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupAutoMoqer(IUnityContainer container)
|
||||
private void SetupAutoMoqer(IContainer container)
|
||||
{
|
||||
_container = container;
|
||||
container.RegisterInstance(this);
|
||||
|
||||
RegisterPlatformLibrary(container);
|
||||
|
||||
_registeredMocks = new Dictionary<Type, object>();
|
||||
AddTheAutoMockingContainerExtensionToTheContainer(container);
|
||||
AssemblyLoader.RegisterSQLiteResolver();
|
||||
}
|
||||
|
||||
private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container)
|
||||
{
|
||||
container.AddNewExtension<AutoMockingContainerExtension>();
|
||||
return;
|
||||
LoadPlatformLibrary();
|
||||
|
||||
AssemblyLoader.RegisterSQLiteResolver();
|
||||
}
|
||||
|
||||
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
|
||||
@@ -177,7 +150,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
|
||||
private bool GetMockHasNotBeenCalledForThisType(Type type)
|
||||
{
|
||||
return _registeredMocks.ContainsKey(type) == false;
|
||||
return !_registeredMocks.ContainsKey(type);
|
||||
}
|
||||
|
||||
private static Type GetTheMockType<T>()
|
||||
@@ -186,7 +159,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
||||
return typeof(T);
|
||||
}
|
||||
|
||||
private void RegisterPlatformLibrary(IUnityContainer container)
|
||||
private void LoadPlatformLibrary()
|
||||
{
|
||||
var assemblyName = "Prowlarr.Windows";
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Moq;
|
||||
using Unity;
|
||||
using Unity.Builder;
|
||||
using Unity.Strategies;
|
||||
|
||||
namespace NzbDrone.Test.Common.AutoMoq.Unity
|
||||
{
|
||||
public class AutoMockingBuilderStrategy : BuilderStrategy
|
||||
{
|
||||
private readonly IUnityContainer _container;
|
||||
private readonly MockRepository _mockFactory;
|
||||
private readonly IEnumerable<Type> _registeredTypes;
|
||||
|
||||
public AutoMockingBuilderStrategy(IEnumerable<Type> registeredTypes, IUnityContainer container)
|
||||
{
|
||||
var autoMoqer = container.Resolve<AutoMoqer>();
|
||||
_mockFactory = new MockRepository(autoMoqer.DefaultBehavior);
|
||||
_registeredTypes = registeredTypes;
|
||||
_container = container;
|
||||
}
|
||||
|
||||
public override void PreBuildUp(ref BuilderContext context)
|
||||
{
|
||||
var autoMoqer = _container.Resolve<AutoMoqer>();
|
||||
|
||||
var type = GetTheTypeFromTheBuilderContext(context);
|
||||
if (AMockObjectShouldBeCreatedForThisType(type))
|
||||
{
|
||||
var mock = CreateAMockObject(type);
|
||||
context.Existing = mock.Object;
|
||||
autoMoqer.SetMock(type, mock);
|
||||
}
|
||||
}
|
||||
|
||||
private bool AMockObjectShouldBeCreatedForThisType(Type type)
|
||||
{
|
||||
var mocker = _container.Resolve<AutoMoqer>();
|
||||
return TypeIsNotRegistered(type) && (mocker.ResolveType == null || mocker.ResolveType != type);
|
||||
}
|
||||
|
||||
private static Type GetTheTypeFromTheBuilderContext(BuilderContext context)
|
||||
{
|
||||
// return (context.OriginalBuildKey).Type;
|
||||
return context.Type;
|
||||
}
|
||||
|
||||
private bool TypeIsNotRegistered(Type type)
|
||||
{
|
||||
return _registeredTypes.Any(x => x.Equals(type)) == false;
|
||||
}
|
||||
|
||||
private Mock CreateAMockObject(Type type)
|
||||
{
|
||||
var createMethod = GenerateAnInterfaceMockCreationMethod(type);
|
||||
|
||||
return InvokeTheMockCreationMethod(createMethod);
|
||||
}
|
||||
|
||||
private Mock InvokeTheMockCreationMethod(MethodInfo createMethod)
|
||||
{
|
||||
return (Mock)createMethod.Invoke(_mockFactory, new object[] { new List<object>().ToArray() });
|
||||
}
|
||||
|
||||
private MethodInfo GenerateAnInterfaceMockCreationMethod(Type type)
|
||||
{
|
||||
var createMethodWithNoParameters = _mockFactory.GetType().GetMethod("Create", EmptyArgumentList());
|
||||
|
||||
return createMethodWithNoParameters.MakeGenericMethod(new[] { type });
|
||||
}
|
||||
|
||||
private static Type[] EmptyArgumentList()
|
||||
{
|
||||
return new[] { typeof(object[]) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Builder;
|
||||
using Unity.Extension;
|
||||
|
||||
namespace NzbDrone.Test.Common.AutoMoq.Unity
|
||||
{
|
||||
public class AutoMockingContainerExtension : UnityContainerExtension
|
||||
{
|
||||
private readonly IList<Type> _registeredTypes = new List<Type>();
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
SetEventsOnContainerToTrackAllRegisteredTypes();
|
||||
SetBuildingStrategyForBuildingUnregisteredTypes();
|
||||
}
|
||||
|
||||
private void SetEventsOnContainerToTrackAllRegisteredTypes()
|
||||
{
|
||||
Context.Registering += (sender, e) => RegisterType(e.TypeFrom);
|
||||
Context.RegisteringInstance += (sender, e) => RegisterType(e.RegisteredType);
|
||||
}
|
||||
|
||||
private void RegisterType(Type typeToRegister)
|
||||
{
|
||||
_registeredTypes.Add(typeToRegister);
|
||||
}
|
||||
|
||||
private void SetBuildingStrategyForBuildingUnregisteredTypes()
|
||||
{
|
||||
var strategy = new AutoMockingBuilderStrategy(_registeredTypes, Container);
|
||||
Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
||||
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||
<PackageReference Include="Unity" Version="5.11.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
@@ -9,4 +10,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Prowlarr.Api.V1.Config
|
||||
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
||||
|
||||
SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
SharedValidator.RuleFor(c => c.InstanceName).StartsOrEndsWithProwlarr();
|
||||
SharedValidator.RuleFor(c => c.InstanceName).ContainsProwlarr().When(c => c.InstanceName.IsNotNullOrWhiteSpace());
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||
|
||||
@@ -4210,44 +4210,6 @@
|
||||
}
|
||||
},
|
||||
"/api/v1/config/ui/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UiConfig"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"UiConfig"
|
||||
@@ -4293,6 +4255,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"UiConfig"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UiConfigResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config/ui": {
|
||||
@@ -6316,6 +6316,10 @@
|
||||
"uiLanguage": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
Reference in New Issue
Block a user