From ede3a55c68359bd3c54b9d4db7b61e79d3969c73 Mon Sep 17 00:00:00 2001 From: Qstick Date: Fri, 15 Dec 2017 23:08:16 -0500 Subject: [PATCH] New: Add Headphones VIP Indexer (#147) * New: Add Headphones VIP Indexer * fixup! String Format Invalid * fixup! Remove hyphen from search string * Add Tests for Headphones Indexer --- .../Files/Indexers/Headphones/Headphones.xml | 377 ++++++++++++++++++ .../HeadphonesCapabilitiesProviderFixture.cs | 98 +++++ .../HeadphonesTests/HeadphonesFixture.cs | 70 ++++ .../NzbDrone.Core.Test.csproj | 5 + .../Indexers/Headphones/Headphones.cs | 73 ++++ .../Headphones/HeadphonesCapabilities.cs | 21 + .../HeadphonesCapabilitiesProvider.cs | 156 ++++++++ .../Headphones/HeadphonesRequestGenerator.cs | 102 +++++ .../Indexers/Headphones/HeadphonesSettings.cs | 61 +++ .../Newznab/NewznabRequestGenerator.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + 11 files changed, 969 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core.Test/Files/Indexers/Headphones/Headphones.xml create mode 100644 src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs create mode 100644 src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs create mode 100644 src/NzbDrone.Core/Indexers/Headphones/Headphones.cs create mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs create mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs create mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs create mode 100644 src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs diff --git a/src/NzbDrone.Core.Test/Files/Indexers/Headphones/Headphones.xml b/src/NzbDrone.Core.Test/Files/Indexers/Headphones/Headphones.xml new file mode 100644 index 000000000..a1129b1a7 --- /dev/null +++ b/src/NzbDrone.Core.Test/Files/Indexers/Headphones/Headphones.xml @@ -0,0 +1,377 @@ + + + + Headphones Indexer + powered by pynab + https://indexer.codeshy.com + + + + Lady Gaga Born This Way 2CD FLAC 2011 WRE + https://indexer.codeshy.com/details/123456 + https://indexer.codeshy.com/api?t=g&guid=123456&apikey=123456789 + Tue, 13 Sep 2016 14:39:52 -0000 + Audio > Lossless + Lady Gaga Born This Way 2CD FLAC 2011 WRE + Sun, 02 Jun 2013 08:58:54 -0000 + alt.binaries.sounds.flac + + 146 + + + + + + + + + + 917347414 + + + + Lady Gaga Born This Way PROMO CDR2 FLAC 2011 WRE + https://indexer.codeshy.com/details/178728 + https://indexer.codeshy.com/api?t=g&guid=178728&apikey=123456789 + Tue, 13 Sep 2016 15:37:26 -0000 + Audio > Lossless + Lady Gaga Born This Way PROMO CDR2 FLAC 2011 WRE + Wed, 19 Sep 2012 18:17:13 -0000 + alt.binaries.sounds.flac + + 4 + + + + + + + + + + 523005229 + + + + Lady Gaga Born This Way PROMO CDR FLAC 2011 WRE + https://indexer.codeshy.com/details/178732 + https://indexer.codeshy.com/api?t=g&guid=178732&apikey=123456789 + Tue, 13 Sep 2016 15:37:27 -0000 + Audio > Lossless + Lady Gaga Born This Way PROMO CDR FLAC 2011 WRE + Wed, 19 Sep 2012 18:12:24 -0000 + alt.binaries.sounds.flac + + 2 + + + + + + + + + + 297599650 + + + + Lady Gaga Born This Way (The Remix) (2011) FLAC + https://indexer.codeshy.com/details/97557 + https://indexer.codeshy.com/api?t=g&guid=97557&apikey=123456789 + Tue, 13 Sep 2016 09:28:03 -0000 + Audio > Lossless + Lady Gaga Born This Way (The Remix) (2011) FLAC + Thu, 17 Nov 2011 15:38:07 -0000 + alt.binaries.sounds.lossless + + 0 + + + + + + + + + + 542418884 + + + + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + https://indexer.codeshy.com/details/97580 + https://indexer.codeshy.com/api?t=g&guid=97580&apikey=123456789 + Tue, 13 Sep 2016 09:28:05 -0000 + Audio > Lossless + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + Thu, 17 Nov 2011 09:21:00 -0000 + alt.binaries.sounds.lossless + + 0 + + + + + + + + + + 44608274 + + + + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + https://indexer.codeshy.com/details/204233 + https://indexer.codeshy.com/api?t=g&guid=204233&apikey=123456789 + Tue, 13 Sep 2016 17:05:21 -0000 + Audio > Lossless + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + Thu, 17 Nov 2011 07:39:41 -0000 + alt.binaries.sounds.flac + + 0 + + + + + + + + + + 42548396 + + + + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + https://indexer.codeshy.com/details/204234 + https://indexer.codeshy.com/api?t=g&guid=204234&apikey=123456789 + Tue, 13 Sep 2016 17:05:22 -0000 + Audio > Lossless + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + Thu, 17 Nov 2011 07:38:23 -0000 + alt.binaries.sounds.flac + + 0 + + + + + + + + + + 514617494 + + + + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + https://indexer.codeshy.com/details/204235 + https://indexer.codeshy.com/api?t=g&guid=204235&apikey=123456789 + Tue, 13 Sep 2016 17:05:22 -0000 + Audio > Lossless + Lady Gaga Born This Way The Remix CD FLAC 2011 EMG + Thu, 17 Nov 2011 07:37:20 -0000 + alt.binaries.sounds.flac + + 0 + + + + + + + + + + 541983521 + + + + Lady Gaga Born This Way (The Remix) 2011 pLAN9 + https://indexer.codeshy.com/details/101273 + https://indexer.codeshy.com/api?t=g&guid=101273&apikey=123456789 + Tue, 13 Sep 2016 10:45:43 -0000 + Audio > MP3 + Lady Gaga Born This Way (The Remix) 2011 pLAN9 + Wed, 16 Nov 2011 23:19:32 -0000 + alt.binaries.sounds.mp3 + + 5 + + + + + + + + + + 12390648 + + + + Lady GaGa Born This Way (Special Edition) 2CD FLAC 2011 PERFECT + https://indexer.codeshy.com/details/214301 + https://indexer.codeshy.com/api?t=g&guid=214301&apikey=123456789 + Tue, 13 Sep 2016 17:38:58 -0000 + Audio > Lossless + Lady GaGa Born This Way (Special Edition) 2CD FLAC 2011 PERFECT + Mon, 17 Oct 2011 16:43:14 -0000 + alt.binaries.sounds.flac + + 5 + + + + + + + + + + 823716079 + + + + Lady GaGa Born This Way Bonus Track CD FLAC 2011 PERFECT + https://indexer.codeshy.com/details/214424 + https://indexer.codeshy.com/api?t=g&guid=214424&apikey=123456789 + Tue, 13 Sep 2016 17:39:21 -0000 + Audio > Lossless + Lady GaGa Born This Way Bonus Track CD FLAC 2011 PERFECT + Mon, 17 Oct 2011 03:37:35 -0000 + alt.binaries.sounds.flac + + 0 + + + + + + + + + + 38894529 + + + + Lady Gaga Born This Way CDM FLAC 2011 WRE + https://indexer.codeshy.com/details/214428 + https://indexer.codeshy.com/api?t=g&guid=214428&apikey=123456789 + Tue, 13 Sep 2016 17:39:22 -0000 + Audio > Lossless + Lady Gaga Born This Way CDM FLAC 2011 WRE + Mon, 17 Oct 2011 03:36:31 -0000 + alt.binaries.sounds.flac + + 1 + + + + + + + + + + 174562763 + + + + Lady GaGa Born This Way Special Edition FLAC + https://indexer.codeshy.com/details/205419 + https://indexer.codeshy.com/api?t=g&guid=205419&apikey=123456789 + Tue, 13 Sep 2016 17:07:40 -0000 + Audio > Lossless + Lady GaGa Born This Way Special Edition FLAC + Tue, 14 Jun 2011 22:06:05 -0000 + alt.binaries.music + + 0 + + + + + + + + + + 8045237 + + + + Lutheria Lady Gaga Born This Way CD1 + https://indexer.codeshy.com/details/205457 + https://indexer.codeshy.com/api?t=g&guid=205457&apikey=123456789 + Tue, 13 Sep 2016 17:07:49 -0000 + Audio > MP3 + Lutheria Lady Gaga Born This Way CD1 + Tue, 31 May 2011 02:04:02 -0000 + alt.binaries.music + + 4 + + + + + + + + + + 4198420 + + + + Lady Gaga Born This Way (New Single) Feb 2011 Mp3ViLLe + https://indexer.codeshy.com/details/24756 + https://indexer.codeshy.com/api?t=g&guid=24756&apikey=123456789 + Tue, 13 Sep 2016 01:29:53 -0000 + Audio > MP3 + Lady Gaga Born This Way (New Single) Feb 2011 Mp3ViLLe + Fri, 11 Mar 2011 11:08:08 -0000 + alt.binaries.sounds.mp3.complete_cd + + 43 + + + + + + + + + + 10301727 + + + + Lady Gaga Born This Way (New Single) Feb 2011 Mp3ViLLe + https://indexer.codeshy.com/details/109954 + https://indexer.codeshy.com/api?t=g&guid=109954&apikey=123456789 + Tue, 13 Sep 2016 11:30:12 -0000 + Audio > MP3 + Lady Gaga Born This Way (New Single) Feb 2011 Mp3ViLLe + Fri, 11 Mar 2011 11:04:06 -0000 + alt.binaries.sounds.mp3 + + 1 + + + + + + + + + + 10301727 + + + diff --git a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs new file mode 100644 index 000000000..ec9567d34 --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesCapabilitiesProviderFixture.cs @@ -0,0 +1,98 @@ +using System; +using System.Xml; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Headphones; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests +{ + [TestFixture] + public class HeadphonesCapabilitiesProviderFixture : CoreTest + { + private HeadphonesSettings _settings; + private string _caps; + + [SetUp] + public void SetUp() + { + _settings = new HeadphonesSettings(); + + _caps = ReadAllText("Files/Indexers/Newznab/newznab_caps.xml"); + } + + private void GivenCapsResponse(string caps) + { + Mocker.GetMock() + .Setup(o => o.Get(It.IsAny())) + .Returns(r => new HttpResponse(r, new HttpHeader(), caps)); + } + + [Test] + public void should_not_request_same_caps_twice() + { + GivenCapsResponse(_caps); + + Subject.GetCapabilities(_settings); + Subject.GetCapabilities(_settings); + + Mocker.GetMock() + .Verify(o => o.Get(It.IsAny()), Times.Once()); + } + + [Test] + public void should_report_pagesize() + { + GivenCapsResponse(_caps); + + var caps = Subject.GetCapabilities(_settings); + + caps.DefaultPageSize.Should().Be(25); + caps.MaxPageSize.Should().Be(60); + } + + [Test] + public void should_use_default_pagesize_if_missing() + { + GivenCapsResponse(_caps.Replace("() + .Setup(o => o.Get(It.IsAny())) + .Throws(); + + Assert.Throws(() => Subject.GetCapabilities(_settings)); + } + + [Test] + public void should_throw_if_xml_invalid() + { + GivenCapsResponse(_caps.Replace("")); + + Assert.Throws(() => Subject.GetCapabilities(_settings)); + } + + [Test] + public void should_not_throw_on_xml_data_unexpected() + { + GivenCapsResponse(_caps.Replace("3040", "asdf")); + + var result = Subject.GetCapabilities(_settings); + + result.Should().NotBeNull(); + + ExceptionVerification.ExpectedErrors(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs new file mode 100644 index 000000000..a577b0a5c --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerTests/HeadphonesTests/HeadphonesFixture.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Headphones; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.IndexerTests.HeadphonesTests +{ + [TestFixture] + public class HeadphonesFixture : CoreTest + { + private HeadphonesCapabilities _caps; + + [SetUp] + public void Setup() + { + Subject.Definition = new IndexerDefinition() + { + Name = "Headphones VIP", + Settings = new HeadphonesSettings() + { + Categories = new int[] { 3000 } + } + }; + + _caps = new HeadphonesCapabilities(); + Mocker.GetMock() + .Setup(v => v.GetCapabilities(It.IsAny())) + .Returns(_caps); + } + + [Test] + public void should_parse_recent_feed_from_headphones() + { + var recentFeed = ReadAllText(@"Files/Indexers/Headphones/Headphones.xml"); + + Mocker.GetMock() + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) + .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); + + var releases = Subject.FetchRecent(); + + releases.Should().HaveCount(16); + + releases.First().Should().BeOfType(); + var releaseInfo = releases.First() as ReleaseInfo; + + releaseInfo.Title.Should().Be("Lady Gaga Born This Way 2CD FLAC 2011 WRE"); + releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); + releaseInfo.DownloadUrl.Should().Be("https://indexer.codeshy.com/api?t=g&guid=123456&apikey=123456789"); + releaseInfo.Indexer.Should().Be(Subject.Definition.Name); + releaseInfo.PublishDate.Should().Be(DateTime.Parse("2013/06/02 08:58:54")); + releaseInfo.Size.Should().Be(917347414); + } + + [Test] + public void should_use_pagesize_reported_by_caps() + { + _caps.MaxPageSize = 30; + _caps.DefaultPageSize = 25; + + Subject.PageSize.Should().Be(25); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 269ceb014..62130c46a 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -221,6 +221,8 @@ + + @@ -390,6 +392,9 @@ sqlite3.dll Always + + Always + Designer Always diff --git a/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs b/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs new file mode 100644 index 000000000..e4b993dd7 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Headphones/Headphones.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Indexers.Newznab; + +namespace NzbDrone.Core.Indexers.Headphones +{ + public class Headphones : HttpIndexerBase + { + private readonly IHeadphonesCapabilitiesProvider _capabilitiesProvider; + + public override string Name => "Headphones VIP"; + + public override DownloadProtocol Protocol => DownloadProtocol.Usenet; + + public override int PageSize => _capabilitiesProvider.GetCapabilities(Settings).DefaultPageSize; + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new HeadphonesRequestGenerator(_capabilitiesProvider) + { + PageSize = PageSize, + Settings = Settings + }; + } + + public override IParseIndexerResponse GetParser() + { + return new NewznabRssParser(); + } + + public Headphones(IHeadphonesCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { + _capabilitiesProvider = capabilitiesProvider; + } + + protected override void Test(List failures) + { + base.Test(failures); + + if (failures.Any()) return; + failures.AddIfNotNull(TestCapabilities()); + } + + protected virtual ValidationFailure TestCapabilities() + { + try + { + var capabilities = _capabilitiesProvider.GetCapabilities(Settings); + + if (capabilities.SupportedSearchParameters != null && capabilities.SupportedSearchParameters.Contains("q")) + { + return null; + } + + return new ValidationFailure(string.Empty, "Indexer does not support required search parameters"); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to indexer: " + ex.Message); + + return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details"); + } + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs new file mode 100644 index 000000000..dacf086e4 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilities.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using NzbDrone.Core.Indexers.Newznab; + +namespace NzbDrone.Core.Indexers.Headphones +{ + public class HeadphonesCapabilities + { + public int DefaultPageSize { get; set; } + public int MaxPageSize { get; set; } + public string[] SupportedSearchParameters { get; set; } + public List Categories { get; set; } + + public HeadphonesCapabilities() + { + DefaultPageSize = 100; + MaxPageSize = 100; + SupportedSearchParameters = new[] { "q" }; + Categories = new List(); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs new file mode 100644 index 000000000..9ac5b8941 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesCapabilitiesProvider.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Xml; +using System.Xml.Linq; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Indexers.Newznab; + +namespace NzbDrone.Core.Indexers.Headphones +{ + public interface IHeadphonesCapabilitiesProvider + { + HeadphonesCapabilities GetCapabilities(HeadphonesSettings settings); + } + + public class HeadphonesCapabilitiesProvider : IHeadphonesCapabilitiesProvider + { + private readonly ICached _capabilitiesCache; + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public HeadphonesCapabilitiesProvider(ICacheManager cacheManager, IHttpClient httpClient, Logger logger) + { + _capabilitiesCache = cacheManager.GetCache(GetType()); + _httpClient = httpClient; + _logger = logger; + } + + public HeadphonesCapabilities GetCapabilities(HeadphonesSettings indexerSettings) + { + var key = indexerSettings.ToJson(); + var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7)); + + return capabilities; + } + + private HeadphonesCapabilities FetchCapabilities(HeadphonesSettings indexerSettings) + { + var capabilities = new HeadphonesCapabilities(); + + var url = string.Format("{0}{1}?t=caps", indexerSettings.BaseUrl.TrimEnd('/'), indexerSettings.ApiPath.TrimEnd('/')); + + if (indexerSettings.ApiKey.IsNotNullOrWhiteSpace()) + { + url += "&apikey=" + indexerSettings.ApiKey; + } + + var request = new HttpRequest(url, HttpAccept.Rss); + + request.AddBasicAuthentication(indexerSettings.Username, indexerSettings.Password); + + HttpResponse response; + + try + { + response = _httpClient.Get(request); + } + catch (Exception ex) + { + _logger.Debug(ex, "Failed to get headphones api capabilities from {0}", indexerSettings.BaseUrl); + throw; + } + + try + { + capabilities = ParseCapabilities(response); + } + catch (XmlException ex) + { + _logger.Debug(ex, "Failed to parse headphones api capabilities for {0}", indexerSettings.BaseUrl); + ex.WithData(response); + throw; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to determine headphones api capabilities for {0}, using the defaults instead till Lidarr restarts", indexerSettings.BaseUrl); + } + + return capabilities; + } + + private HeadphonesCapabilities ParseCapabilities(HttpResponse response) + { + var capabilities = new HeadphonesCapabilities(); + + var xDoc = XDocument.Parse(response.Content); + + if (xDoc == null) + { + throw new XmlException("Invalid XML"); + } + + var xmlRoot = xDoc.Element("caps"); + + if (xmlRoot == null) + { + throw new XmlException("Unexpected XML"); + } + + var xmlLimits = xmlRoot.Element("limits"); + if (xmlLimits != null) + { + capabilities.DefaultPageSize = int.Parse(xmlLimits.Attribute("default").Value); + capabilities.MaxPageSize = int.Parse(xmlLimits.Attribute("max").Value); + } + + var xmlSearching = xmlRoot.Element("searching"); + if (xmlSearching != null) + { + var xmlBasicSearch = xmlSearching.Element("search"); + if (xmlBasicSearch == null || xmlBasicSearch.Attribute("available").Value != "yes") + { + capabilities.SupportedSearchParameters = null; + } + else if (xmlBasicSearch.Attribute("supportedParams") != null) + { + capabilities.SupportedSearchParameters = xmlBasicSearch.Attribute("supportedParams").Value.Split(','); + } + } + + var xmlCategories = xmlRoot.Element("categories"); + if (xmlCategories != null) + { + foreach (var xmlCategory in xmlCategories.Elements("category")) + { + var cat = new NewznabCategory + { + Id = int.Parse(xmlCategory.Attribute("id").Value), + Name = xmlCategory.Attribute("name").Value, + Description = xmlCategory.Attribute("description") != null ? xmlCategory.Attribute("description").Value : string.Empty, + Subcategories = new List() + }; + + foreach (var xmlSubcat in xmlCategory.Elements("subcat")) + { + cat.Subcategories.Add(new NewznabCategory + { + Id = int.Parse(xmlSubcat.Attribute("id").Value), + Name = xmlSubcat.Attribute("name").Value, + Description = xmlSubcat.Attribute("description") != null ? xmlSubcat.Attribute("description").Value : string.Empty + + }); + } + + capabilities.Categories.Add(cat); + } + } + + return capabilities; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs new file mode 100644 index 000000000..44a649bf2 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesRequestGenerator.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.Indexers.Headphones +{ + public class HeadphonesRequestGenerator : IIndexerRequestGenerator + { + private readonly IHeadphonesCapabilitiesProvider _capabilitiesProvider; + public int MaxPages { get; set; } + public int PageSize { get; set; } + public HeadphonesSettings Settings { get; set; } + + public HeadphonesRequestGenerator(IHeadphonesCapabilitiesProvider capabilitiesProvider) + { + _capabilitiesProvider = capabilitiesProvider; + + MaxPages = 30; + PageSize = 100; + } + + public virtual IndexerPageableRequestChain GetRecentRequests() + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", "")); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.AddTier(); + + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", + string.Format("&q={0}", + NewsnabifyTitle(string.Format("{0} {1}", + searchCriteria.Artist.Name, + searchCriteria.AlbumTitle))))); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.AddTier(); + + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", + string.Format("&q={0}", + NewsnabifyTitle(searchCriteria.Artist.Name)))); + + return pageableRequests; + } + + private IEnumerable GetPagedRequests(int maxPages, IEnumerable categories, string searchType, string parameters) + { + if (categories.Empty()) + { + yield break; + } + + var categoriesQuery = string.Join(",", categories.Distinct()); + + var baseUrl = string.Format("{0}{1}?t={2}&cat={3}&extended=1", Settings.BaseUrl.TrimEnd('/'), Settings.ApiPath.TrimEnd('/'), searchType, categoriesQuery); + + if (Settings.ApiKey.IsNotNullOrWhiteSpace()) + { + baseUrl += "&apikey=" + Settings.ApiKey; + } + + if (PageSize == 0) + { + var request = new IndexerRequest($"{baseUrl}{parameters}", HttpAccept.Rss); + request.HttpRequest.AddBasicAuthentication(Settings.Username, Settings.Password); + + yield return request; + } + else + { + for (var page = 0; page < maxPages; page++) + { + var request = new IndexerRequest(string.Format("{0}&offset={1}&limit={2}{3}", baseUrl, page * PageSize, PageSize, parameters), HttpAccept.Rss); + request.HttpRequest.AddBasicAuthentication(Settings.Username, Settings.Password); + + yield return request; + } + } + } + + private static string NewsnabifyTitle(string title) + { + return title.Replace("+", "%20"); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs new file mode 100644 index 000000000..d3fc568b8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/Headphones/HeadphonesSettings.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.Headphones +{ + public class HeadphonesSettingsValidator : AbstractValidator + { + public HeadphonesSettingsValidator() + { + Custom(newznab => + { + if (newznab.Categories.Empty()) + { + return new ValidationFailure("", "'Categories' must be provided"); + } + + return null; + }); + + RuleFor(c => c.Username).NotEmpty(); + RuleFor(c => c.Password).NotEmpty(); + } + } + + public class HeadphonesSettings : IIndexerSettings + { + private static readonly HeadphonesSettingsValidator Validator = new HeadphonesSettingsValidator(); + + public HeadphonesSettings() + { + ApiPath = "/api"; + BaseUrl = "https://indexer.codeshy.com"; + ApiKey = "964d601959918a578a670984bdee9357"; + Categories = new[] { 3000, 3010, 3020, 3030, 3040 }; + } + + public string BaseUrl { get; set; } + + public string ApiPath { get; set; } + + public string ApiKey { get; set; } + + [FieldDefinition(0, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows", Advanced = true)] + public IEnumerable Categories { get; set; } + + [FieldDefinition(1, Label = "Username")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Password", Type = FieldType.Password)] + public string Password { get; set; } + + public virtual NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index ad3676fc5..389aeed82 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -81,7 +81,7 @@ namespace NzbDrone.Core.Indexers.Newznab pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", string.Format("&q={0}", - NewsnabifyTitle(string.Format("{0} - {1}", + NewsnabifyTitle(string.Format("{0} {1}", searchCriteria.Artist.Name, searchCriteria.AlbumTitle))))); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 55668eea2..690a78315 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -521,6 +521,11 @@ + + + + +