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