Compare commits

..

17 Commits

Author SHA1 Message Date
Qstick
d2cf060473 Fixed: (LazyLibrarian) Use listNabProviders instead of listProviders 2021-12-06 16:37:16 -06:00
Qstick
3b7b72d4e1 Fixed: (Cardigann) Always use search headers for download if defined 2021-12-05 17:43:15 -06:00
Qstick
4e69b80a98 Bump to 0.1.7 2021-12-05 17:29:22 -06:00
Qstick
0f52258d53 Fixed: (Flaresolverr) YggCookie and YggTorrent Issues 2021-12-05 17:26:58 -06:00
Qstick
4eadd4cb2f New: LazyLibrarian Sync Support
Closes #469

Co-Authored-By: philborman <12158777+philborman@users.noreply.github.com>
2021-12-05 11:41:51 -06:00
Qstick
579b8a3d3b New: (Cardigann) More feed metadata for book and music 2021-12-05 11:23:47 -06:00
bakerboy448
849b3de7d3 readme updates [skip ci] 2021-12-05 13:21:50 +00:00
Robin Dadswell
8855b2846d Fixed: Updated wording of Application Server URLs 2021-12-05 07:21:15 -06:00
bakerboy448
c64addb976 Fixed: (UNIT3D Indexers) Cleanse RID in Logs
Fixes #652
2021-12-04 18:51:33 -06:00
Qstick
fab1304bcd Skip DB backup on Postgres DB 2021-12-04 18:50:33 -06:00
Qstick
bd834fb4d7 Fixed: Stats fails to load due to unparsable elapsedTime for history event
Fixes #663
2021-12-04 18:03:33 -06:00
Qstick
dcee9582bd Speedup Stats endpoint call X3 2021-12-04 17:24:20 -06:00
Qstick
89e500edfd Fixed: (Stats) All filter not returning all 2021-12-04 17:18:20 -06:00
Qstick
ea83020714 Build Magnet on Cardigann separate 2021-12-04 17:17:47 -06:00
Qstick
6d62744667 Fixed: (Cardigann) Magnet generation for public indexers with InfoHash
Fixes #668
2021-12-04 12:26:55 -06:00
Qstick
08c68e26c1 Fixed: Correctly return infohash in torznab response when available 2021-12-04 12:10:19 -06:00
Qstick
574568e71d Optimize HandleJsonSelector() to avoid needless throws 2021-12-04 12:09:23 -06:00
29 changed files with 672 additions and 72 deletions

View File

@@ -4,74 +4,84 @@
[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Prowlarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Prowlarr/megasponsors/badge.svg)](#mega-sponsors)
Prowlarr is an indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
## Major Features Include:
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
## Major Features Include
- Usenet support for 24 indexers natively, including Headphones VIP
- Usenet support for any Newznab compatible indexer via "Generic Newznab"
- Torrent support for over 500 trackers with more added all the time
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr/Mylar3, so no manual configuration of the other applications are required
- Support for custom YML definitions via Cardigann that includes JSON and XML parsing
- Indexer Sync to Lidarr/Mylar3/Radarr/Readarr/Sonarr, so no manual configuration of the other applications are required
- Indexer history and statistics
- Manual searching of Trackers & Indexers at a category level
- Support for pushing releases directly to your download clients from Prowlarr
- Parameter based manual searching
- Support for pushing multiple releases at once directly to your download clients from Prowlarr
- Indexer health and status notifications
- Per Indexer proxy support (SOCKS4, SOCKS5, HTTP, Flaresolverr)
## Support
Note: Prowlarr is currently early in life, thus bugs should be expected
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Prowlarr/Prowlarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
## Indexers/Trackers
## Indexers & Trackers
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
[![Supported Indexers](https://img.shields.io/badge/Supported%20Indexers-View%20all%20currently%20supported%20indexers%20%26%20trackers-important)](https://wiki.servarr.com/en/prowlarr/supported-indexers)
[Indexer Requests](https://requests.prowlarr.com)
- Request or vote on an existing request for a new tracker/indexer
[![Indexer Requests](https://img.shields.io/badge/Indexer%20Requests-Create%20and%20view%20existing%20requests%20for%20trackers%20and%20indexers-informational)](https://requests.prowlarr.com)
## Contributors & Developers
[API Documentation](https://prowlarr.com/docs/api/)
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/prowlarr/contributing)
- [YML Indexer Defintion (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
- [YML Indexer Definition (Wiki Article)](https://wiki.servarr.com/prowlarr/cardigann-yml-definition)
This project exists thanks to all the people who contribute.
<a href="https://github.com/Prowlarr/Prowlarr/graphs/contributors"><img src="https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false" /></a>
[![Contributors List](https://opencollective.com/Prowlarr/contributors.svg?width=890&button=false)](https://github.com/Prowlarr/Prowlarr/graphs/contributors)
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Prowlarr#backer)
<img src="https://opencollective.com/Prowlarr/backers.svg?width=890"></a>
![Backers List](https://opencollective.com/Prowlarr/backers.svg?width=890)
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Prowlarr#sponsor)
<img src="https://opencollective.com/Prowlarr/sponsors.svg?width=890"></a>
![Sponsors List](https://opencollective.com/Prowlarr/sponsors.svg?width=890)
## Mega Sponsors
<img src="https://opencollective.com/Prowlarr/tiers/mega-sponsor.svg?width=890"></a>
![Mega Sponsors List](https://opencollective.com/Prowlarr/tiers/mega-sponsor.svg?width=890)
## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
- [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
- [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
- [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
- [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2021
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2022
Icon Credit:
<a href="https://www.freepik.com/vectors/box">Box vector created by freepik - www.freepik.com</a>
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)

View File

@@ -7,7 +7,7 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
majorVersion: '0.1.6'
majorVersion: '0.1.7'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'

View File

@@ -26,17 +26,21 @@ namespace NzbDrone.Common.Test.InstrumentationTests
//Indexer Responses
// avistaz response
// avistaz response
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
// danish bytes response
// danish bytes response
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
// nzbgeek & usenet response
// nzbgeek & usenet response
[TestCase(@"<guid isPermaLink=""true"">https://api.nzbgeek.info/api?t=details&amp;id=2b51db35e1910123321025a12b9933d2&amp;apikey=2b51db35e1910123321025a12b9933d2</guid>")]
// UNIT3D Response
[TestCase(@"""download_link"":""https://blutopia.xyz/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@"""download_link"":""https://desitorrents.tv/torrent/download/114592.2b51db35e1910123321025a12b9933d2"",")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
[TestCase(@"{ ""Name"" : ""Server1.Username"", ""Value"" : ""mySecret"" }, { ""Name"" : ""Server1.Password"", ""Value"" : ""mySecret"" }, ")]

View File

@@ -254,7 +254,8 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.TransferEncoding = header.Value;
break;
case "User-Agent":
throw new NotSupportedException("User-Agent other than Prowlarr not allowed.");
webRequest.UserAgent = header.Value;
break;
case "Proxy-Connection":
throw new NotImplementedException();
default:

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -107,6 +107,18 @@ namespace NzbDrone.Common.Http
}
}
public string UserAgent
{
get
{
return GetSingleValue("User-Agent");
}
set
{
SetSingleValue("User-Agent", value);
}
}
public string Accept
{
get

View File

@@ -21,6 +21,9 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// UNIT3D
new Regex(@"(?<=[a-z0-9-]+\.[a-z]+/torrent/download/\d+\.)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarian : ApplicationBase<LazyLibrarianSettings>
{
public override string Name => "LazyLibrarian";
private readonly ILazyLibrarianV1Proxy _lazyLibrarianV1Proxy;
private readonly IConfigFileProvider _configFileProvider;
public LazyLibrarian(ILazyLibrarianV1Proxy lazyLibrarianV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_lazyLibrarianV1Proxy = lazyLibrarianV1Proxy;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
try
{
failures.AddIfNotNull(_lazyLibrarianV1Proxy.TestConnection(Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to LazyLibrarian"));
}
return new ValidationResult(failures);
}
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _lazyLibrarianV1Proxy.GetIndexers(Settings);
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
if (indexer.Apikey == _configFileProvider.ApiKey)
{
var match = AppIndexerRegex.Match(indexer.Host);
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(new AppIndexerMap { RemoteIndexerName = $"{indexer.Type},{indexer.Name}", IndexerId = indexerId });
}
}
}
return mappings;
}
public override void AddIndexer(IndexerDefinition indexer)
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol);
var remoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{remoteIndexer.Type},{remoteIndexer.Name}" });
}
}
public override void RemoveIndexer(int indexerId)
{
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
if (indexerMapping != null)
{
//Remove Indexer remotely and then remove the mapping
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
_lazyLibrarianV1Proxy.RemoveIndexer(indexerProps[1], (LazyLibrarianProviderType)Enum.Parse(typeof(LazyLibrarianProviderType), indexerProps[0]), Settings);
_appIndexerMapService.Delete(indexerMapping.Id);
}
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
var indexerProps = indexerMapping.RemoteIndexerName.Split(",");
var lazyLibrarianIndexer = BuildLazyLibrarianIndexer(indexer, indexer.Protocol, indexerProps[1]);
//Use the old remote id to find the indexer on LazyLibrarian incase the update was from a name change in Prowlarr
var remoteIndexer = _lazyLibrarianV1Proxy.GetIndexer(indexerProps[1], lazyLibrarianIndexer.Type, Settings);
if (remoteIndexer != null)
{
_logger.Debug("Remote indexer found, syncing with current settings");
if (!lazyLibrarianIndexer.Equals(remoteIndexer))
{
_lazyLibrarianV1Proxy.UpdateIndexer(lazyLibrarianIndexer, Settings);
indexerMapping.RemoteIndexerName = $"{lazyLibrarianIndexer.Type},{lazyLibrarianIndexer.Altername}";
_appIndexerMapService.Update(indexerMapping);
}
}
else
{
_appIndexerMapService.Delete(indexerMapping.Id);
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
_logger.Debug("Remote indexer not found, re-adding {0} to LazyLibrarian", indexer.Name);
var newRemoteIndexer = _lazyLibrarianV1Proxy.AddIndexer(lazyLibrarianIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerName = $"{newRemoteIndexer.Type},{newRemoteIndexer.Name}" });
}
else
{
_logger.Debug("Remote indexer not found for {0}, skipping re-add to LazyLibrarian due to indexer capabilities", indexer.Name);
}
}
}
private LazyLibrarianIndexer BuildLazyLibrarianIndexer(IndexerDefinition indexer, DownloadProtocol protocol, string originalName = null)
{
var schema = protocol == DownloadProtocol.Usenet ? LazyLibrarianProviderType.Newznab : LazyLibrarianProviderType.Torznab;
var lazyLibrarianIndexer = new LazyLibrarianIndexer
{
Name = originalName ?? $"{indexer.Name} (Prowlarr)",
Altername = $"{indexer.Name} (Prowlarr)",
Host = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api",
Apikey = _configFileProvider.ApiKey,
Categories = string.Join(",", indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())),
Enabled = indexer.Enable,
Type = schema,
};
return lazyLibrarianIndexer;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarianError
{
public int Code { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarianException : NzbDroneException
{
public LazyLibrarianException(string message)
: base(message)
{
}
public LazyLibrarianException(string message, params object[] args)
: base(message, args)
{
}
public LazyLibrarianException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarianIndexerResponse
{
public bool Success { get; set; }
public LazyLibrarianIndexerData Data { get; set; }
public LazyLibrarianError Error { get; set; }
}
public class LazyLibrarianIndexerData
{
public List<LazyLibrarianIndexer> Torznabs { get; set; }
public List<LazyLibrarianIndexer> Newznabs { get; set; }
}
public enum LazyLibrarianProviderType
{
Newznab,
Torznab
}
public class LazyLibrarianIndexer
{
public string Name { get; set; }
public string Host { get; set; }
public string Apikey { get; set; }
public string Categories { get; set; }
public bool Enabled { get; set; }
public string Altername { get; set; }
public LazyLibrarianProviderType Type { get; set; }
public bool Equals(LazyLibrarianIndexer other)
{
if (ReferenceEquals(null, other))
{
return false;
}
return other.Host == Host &&
other.Apikey == Apikey &&
other.Name == Name &&
other.Categories == Categories &&
other.Enabled == Enabled &&
other.Altername == Altername;
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarianSettingsValidator : AbstractValidator<LazyLibrarianSettings>
{
public LazyLibrarianSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
RuleFor(c => c.SyncCategories).NotEmpty();
}
}
public class LazyLibrarianSettings : IApplicationSettings
{
private static readonly LazyLibrarianSettingsValidator Validator = new LazyLibrarianSettingsValidator();
public LazyLibrarianSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:5299";
SyncCategories = new[]
{
NewznabStandardCategory.AudioAudiobook.Id,
NewznabStandardCategory.Books.Id,
NewznabStandardCategory.BooksComics.Id,
NewznabStandardCategory.BooksEBook.Id,
NewznabStandardCategory.BooksForeign.Id,
NewznabStandardCategory.BooksMags.Id,
NewznabStandardCategory.BooksOther.Id,
NewznabStandardCategory.BooksTechnical.Id,
};
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as LazyLibrarian sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "LazyLibrarian Server", HelpText = "URL used to connect to LazyLibrarian server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by LazyLibrarian in Settings/Web Interface")]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> SyncCategories { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public class LazyLibrarianStatus
{
public bool Success { get; set; }
public LazyLibrarianError Error { get; set; }
}
}

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Applications.LazyLibrarian
{
public interface ILazyLibrarianV1Proxy
{
LazyLibrarianIndexer AddIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings);
List<LazyLibrarianIndexer> GetIndexers(LazyLibrarianSettings settings);
LazyLibrarianIndexer GetIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings);
void RemoveIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings);
LazyLibrarianIndexer UpdateIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings);
ValidationFailure TestConnection(LazyLibrarianSettings settings);
}
public class LazyLibrarianV1Proxy : ILazyLibrarianV1Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public LazyLibrarianV1Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public LazyLibrarianStatus GetStatus(LazyLibrarianSettings settings)
{
var request = BuildRequest(settings, "/api", "getVersion", HttpMethod.GET);
return Execute<LazyLibrarianStatus>(request);
}
public List<LazyLibrarianIndexer> GetIndexers(LazyLibrarianSettings settings)
{
var request = BuildRequest(settings, "/api", "listNabProviders", HttpMethod.GET);
var response = Execute<LazyLibrarianIndexerResponse>(request);
if (!response.Success)
{
throw new LazyLibrarianException(string.Format("LazyLibrarian Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
}
var indexers = new List<LazyLibrarianIndexer>();
var torIndexers = response.Data.Torznabs;
torIndexers.ForEach(i => i.Type = LazyLibrarianProviderType.Torznab);
var nzbIndexers = response.Data.Newznabs;
nzbIndexers.ForEach(i => i.Type = LazyLibrarianProviderType.Newznab);
indexers.AddRange(torIndexers);
indexers.AddRange(nzbIndexers);
indexers.ForEach(i => i.Altername = i.Name);
return indexers;
}
public LazyLibrarianIndexer GetIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings)
{
var indexers = GetIndexers(settings);
return indexers.SingleOrDefault(i => i.Name == indexerName && i.Type == indexerType);
}
public void RemoveIndexer(string indexerName, LazyLibrarianProviderType indexerType, LazyLibrarianSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexerName },
{ "providertype", indexerType.ToString().ToLower() }
};
var request = BuildRequest(settings, "/api", "delProvider", HttpMethod.GET, parameters);
CheckForError(Execute<LazyLibrarianStatus>(request));
}
public LazyLibrarianIndexer AddIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexer.Name },
{ "providertype", indexer.Type.ToString().ToLower() },
{ "host", indexer.Host },
{ "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories }
};
var request = BuildRequest(settings, "/api", "addProvider", HttpMethod.GET, parameters);
CheckForError(Execute<LazyLibrarianStatus>(request));
return indexer;
}
public LazyLibrarianIndexer UpdateIndexer(LazyLibrarianIndexer indexer, LazyLibrarianSettings settings)
{
var parameters = new Dictionary<string, string>
{
{ "name", indexer.Name },
{ "providertype", indexer.Type.ToString().ToLower() },
{ "host", indexer.Host },
{ "prov_apikey", indexer.Apikey },
{ "enabled", indexer.Enabled.ToString().ToLower() },
{ "categories", indexer.Categories },
{ "altername", indexer.Altername }
};
var request = BuildRequest(settings, "/api", "changeProvider", HttpMethod.GET, parameters);
CheckForError(Execute<LazyLibrarianStatus>(request));
return indexer;
}
private void CheckForError(LazyLibrarianStatus response)
{
if (!response.Success)
{
throw new LazyLibrarianException(string.Format("LazyLibrarian Error - Code {0}: {1}", response.Error.Code, response.Error.Message));
}
}
public ValidationFailure TestConnection(LazyLibrarianSettings settings)
{
try
{
var status = GetStatus(settings);
if (!status.Success)
{
return new ValidationFailure("ApiKey", status.Error.Message);
}
}
catch (HttpException ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(LazyLibrarianSettings settings, string resource, string command, HttpMethod method, Dictionary<string, string> parameters = null)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var requestBuilder = new HttpRequestBuilder(baseUrl).Resource(resource)
.AddQueryParam("cmd", command)
.AddQueryParam("apikey", settings.ApiKey);
if (parameters != null)
{
foreach (var param in parameters)
{
requestBuilder.AddQueryParam(param.Key, param.Value);
}
}
var request = requestBuilder.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
request.AllowAutoRedirect = true;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}

View File

@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Applications.Lidarr
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "URL used to connect to Lidarr server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")]

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Applications.Mylar
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Mylar sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Mylar Server", HelpText = "Mylar server URL, including http(s):// and port if needed")]
[FieldDefinition(1, Label = "Mylar Server", HelpText = "URL used to connect to Mylar server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Mylar in Settings/Web Interface")]

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Applications.Radarr
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")]
[FieldDefinition(1, Label = "Radarr Server", HelpText = "URL used to connect to Radarr server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")]

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Applications.Readarr
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]
[FieldDefinition(1, Label = "Readarr Server", HelpText = "URL used to connect to Readarr server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")]

View File

@@ -31,7 +31,7 @@ namespace NzbDrone.Core.Applications.Sonarr
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "URL used to connect to Sonarr server, including http(s)://, port, and urlbase if required")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Sonarr in Settings/General")]

View File

@@ -187,9 +187,12 @@ namespace NzbDrone.Core.Backup
private void BackupDatabase()
{
_logger.ProgressDebug("Backing up database");
if (_maindDb.DatabaseType == DatabaseType.SQLite)
{
_logger.ProgressDebug("Backing up database");
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
}
}
private void BackupConfigFile()

View File

@@ -5,7 +5,9 @@ using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Localization;
@@ -16,10 +18,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
public class FlareSolverr : HttpIndexerProxyBase<FlareSolverrSettings>
{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string> { "cloudflare", "cloudflare-nginx" };
private readonly ICached<string> _cache;
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService)
public FlareSolverr(IProwlarrCloudRequestBuilder cloudRequestBuilder, IHttpClient httpClient, Logger logger, ILocalizationService localizationService, ICacheManager cacheManager)
: base(cloudRequestBuilder, httpClient, logger, localizationService)
{
_cache = cacheManager.GetCache<string>(typeof(string), "UserAgent");
}
public override string Name => "FlareSolverr";
@@ -29,6 +33,12 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
//Try original request first, ignore errors, detect CF in post response
request.SuppressHttpError = true;
//Inject UA if not present
if (_cache.Find(request.Url.Host).IsNotNullOrWhiteSpace() && request.Headers.UserAgent.IsNullOrWhiteSpace())
{
request.Headers.UserAgent = _cache.Find(request.Url.Host);
}
return request;
}
@@ -51,18 +61,18 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
result = JsonConvert.DeserializeObject<FlareSolverrResponse>(flaresolverrResponse.Content);
var cookieCollection = new CookieCollection();
var responseHeader = new HttpHeader();
var newRequest = response.Request;
foreach (var cookie in result.Solution.Cookies)
{
cookieCollection.Add(cookie.ToCookieObj());
}
//Cache the user-agent so we can inject it in next request to avoid re-solve
_cache.Set(response.Request.Url.Host, result.Solution.UserAgent);
newRequest.Headers.UserAgent = result.Solution.UserAgent;
//Build new response with FS Cookie and Site Response
var newResponse = new HttpResponse(response.Request, responseHeader, cookieCollection, result.Solution.Response);
InjectCookies(newRequest, result);
return newResponse;
//Request again with User-Agent and Cookies from Flaresolvrr
var finalResponse = _httpClient.Execute(newRequest);
return finalResponse;
}
private static bool IsCloudflareProtected(HttpResponse response)
@@ -79,6 +89,24 @@ namespace NzbDrone.Core.IndexerProxies.FlareSolverr
return false;
}
private void InjectCookies(HttpRequest request, FlareSolverrResponse flareSolverrResponse)
{
var rCookies = flareSolverrResponse.Solution.Cookies;
if (!rCookies.Any())
{
return;
}
var rCookiesList = rCookies.Select(x => x.Name).ToList();
foreach (var rCookie in rCookies)
{
request.Cookies.Remove(rCookie.Name);
request.Cookies.Add(rCookie.Name, rCookie.Value);
}
}
private HttpRequest GenerateFlareSolverrRequest(HttpRequest request)
{
FlareSolverrRequest req;

View File

@@ -92,12 +92,16 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("rageid", r.TvRageId, protocol),
GetNabElement("tvdbid", r.TvdbId, protocol),
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
GetNabElement("tmdb", r.TmdbId, protocol),
GetNabElement("tmdbid", r.TmdbId, protocol),
GetNabElement("seeders", t.Seeders, protocol),
GetNabElement("files", r.Files, protocol),
GetNabElement("grabs", r.Grabs, protocol),
GetNabElement("peers", t.Peers, protocol),
GetNabElement("infohash", RemoveInvalidXMLChars(r.Guid), protocol),
GetNabElement("author", RemoveInvalidXMLChars(r.Author), protocol),
GetNabElement("booktitle", RemoveInvalidXMLChars(r.BookTitle), protocol),
GetNabElement("artist", RemoveInvalidXMLChars(r.Artist), protocol),
GetNabElement("album", RemoveInvalidXMLChars(r.Album), protocol),
GetNabElement("infohash", RemoveInvalidXMLChars(t.InfoHash), protocol),
GetNabElement("minimumratio", t.MinimumRatio, protocol),
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),
GetNabElement("downloadvolumefactor", t.DownloadVolumeFactor, protocol),

View File

@@ -54,8 +54,11 @@ namespace NzbDrone.Core.IndexerStats
var sortedEvents = indexer.OrderBy(v => v.Date)
.ThenBy(v => v.Id)
.ToArray();
int temp = 0;
indexerStats.AverageResponseTime = (int)sortedEvents.Where(h => h.Data.ContainsKey("elapsedTime")).Select(h => int.Parse(h.Data.GetValueOrDefault("elapsedTime"))).Average();
indexerStats.AverageResponseTime = (int)sortedEvents.Where(h => int.TryParse(h.Data.GetValueOrDefault("elapsedTime"), out temp))
.Select(h => temp)
.Average();
foreach (var historyEvent in sortedEvents)
{

View File

@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
return ApplyFilters(value.Trim(), selector.Filters, variables);
return ApplyFilters(value?.Trim(), selector.Filters, variables) ?? null;
}
protected Dictionary<string, object> GetBaseTemplateVariables()

View File

@@ -358,6 +358,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
releases = releases.Take(query.Limit).ToList();
}*/
releases.ForEach(c =>
{
// generate magnet link from info hash (not allowed for private sites)
if (((TorrentInfo)c).MagnetUrl == null && !string.IsNullOrWhiteSpace(((TorrentInfo)c).InfoHash) && _definition.Type != "private")
{
((TorrentInfo)c).MagnetUrl = MagnetLinkBuilder.BuildPublicMagnetLink(((TorrentInfo)c).InfoHash, c.Title);
}
// generate info hash from magnet link
if (((TorrentInfo)c).MagnetUrl != null && string.IsNullOrWhiteSpace(((TorrentInfo)c).InfoHash))
{
((TorrentInfo)c).InfoHash = MagnetLinkBuilder.GetInfoHashFromMagnet(((TorrentInfo)c).MagnetUrl);
}
});
_logger.Debug($"Got {releases.Count} releases");
return releases;
@@ -546,13 +561,18 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.PosterUrl;
break;
//case "author":
// release.Author = value;
// break;
//case "booktitle":
// release.BookTitle = value;
// break;
case "author":
release.Author = value;
break;
case "booktitle":
release.BookTitle = value;
break;
case "artist":
release.Artist = value;
break;
case "album":
release.Album = value;
break;
default:
break;
}

View File

@@ -727,14 +727,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
var method = HttpMethod.GET;
var headers = new Dictionary<string, string>();
var variables = GetBaseTemplateVariables();
AddTemplateVariablesFromUri(variables, link, ".DownloadUri");
headers = ParseCustomHeaders(_definition.Search?.Headers, variables);
if (_definition.Download != null)
{
var download = _definition.Download;
var variables = GetBaseTemplateVariables();
AddTemplateVariablesFromUri(variables, link, ".DownloadUri");
headers = ParseCustomHeaders(_definition.Search?.Headers, variables);
HttpResponse response = null;
var request = new HttpRequestBuilder(link.ToString())

View File

@@ -132,10 +132,13 @@ namespace NzbDrone.Core.Indexers
c.DownloadProtocol = Protocol;
c.IndexerPriority = ((IndexerDefinition)Definition).Priority;
//Add common flags
if (Protocol == DownloadProtocol.Torrent && ((TorrentInfo)c).DownloadVolumeFactor == 0)
if (Protocol == DownloadProtocol.Torrent)
{
c.IndexerFlags.Add(IndexerFlag.FreeLeech);
//Add common flags
if (((TorrentInfo)c).DownloadVolumeFactor == 0)
{
((TorrentInfo)c).IndexerFlags.Add(IndexerFlag.FreeLeech);
}
}
});

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MonoTorrent;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers
{
@@ -33,5 +36,18 @@ namespace NzbDrone.Core.Indexers
{
return new MagnetLink(InfoHash.FromHex(infoHash), releaseTitle, _trackers).ToV1String();
}
public static string GetInfoHashFromMagnet(string magnet)
{
try
{
var xt = ParseUtil.GetArgumentFromQueryString(magnet.ToString(), "xt");
return xt.Split(':').Last(); // remove prefix urn:btih:
}
catch (Exception)
{
return null;
}
}
}
}

View File

@@ -31,6 +31,10 @@ namespace NzbDrone.Core.Parser.Model
public int TvRageId { get; set; }
public int ImdbId { get; set; }
public int TmdbId { get; set; }
public string Author { get; set; }
public string BookTitle { get; set; }
public string Artist { get; set; }
public string Album { get; set; }
public DateTime PublishDate { get; set; }
public string PosterUrl { get; set; }
@@ -93,6 +97,7 @@ namespace NzbDrone.Core.Parser.Model
stringBuilder.AppendLine("TvdbId: " + TvdbId ?? "Empty");
stringBuilder.AppendLine("TvRageId: " + TvRageId ?? "Empty");
stringBuilder.AppendLine("ImdbId: " + ImdbId ?? "Empty");
stringBuilder.AppendLine("TmdbId: " + TmdbId ?? "Empty");
stringBuilder.AppendLine("PublishDate: " + PublishDate ?? "Empty");
return stringBuilder.ToString();
default:

View File

@@ -18,14 +18,16 @@ namespace Prowlarr.Api.V1.Indexers
[HttpGet]
public IndexerStatsResource GetAll(DateTime? startDate, DateTime? endDate)
{
var statsStartDate = startDate ?? DateTime.Now.AddDays(-30);
var statsStartDate = startDate ?? DateTime.MinValue;
var statsEndDate = endDate ?? DateTime.Now;
var indexerStats = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate);
var indexerResource = new IndexerStatsResource
{
Indexers = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).IndexerStatistics,
UserAgents = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).UserAgentStatistics,
Hosts = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate).HostStatistics
Indexers = indexerStats.IndexerStatistics,
UserAgents = indexerStats.UserAgentStatistics,
Hosts = indexerStats.HostStatistics
};
return indexerResource;