New: Titans of TV tracker

pull/6/head
Mark McDowall 10 years ago committed by Taloth Saldono
parent c6c68c0c75
commit 58b01b91d5

@ -0,0 +1,67 @@
{
"code": "SUCCESSFUL",
"http_code": 200,
"limit": "2",
"offset": 0,
"results": [
{
"air_date": "20150623",
"anonymous": 1,
"codec": "x264",
"container": "MKV",
"created_at": "2015-06-25 04:13:44",
"download": "https://titansof.tv/api/torrents/19445/download?apikey=abc",
"ecommentUrl": "https://titansof.tv/series/287053/episode/5453241#comments",
"episode": "S02E04",
"episodeUrl": "https://titansof.tv/series/287053/episode/5453241",
"episode_id": "5453241",
"id": "19445",
"language": "en",
"leechers": 5,
"network": "truTV",
"origin": "Scene",
"release_name": "Series.Title.S02E04.720p.HDTV.x264-W4F",
"resolution": "720p",
"season": "",
"season_id": 0,
"seeders": 2,
"series": "Series Title",
"series_id": "287053",
"size": 435402993,
"snatched": 0,
"source": "HDTV",
"updated_at": "2015-06-25 04:13:44",
"user_id": 0
},
{
"air_date": "20150624",
"anonymous": 1,
"codec": "x264",
"container": "MKV",
"created_at": "2015-06-25 04:11:59",
"download": "https://titansof.tv/api/torrents/19444/download?apikey=abc",
"ecommentUrl": "https://titansof.tv/series/75382/episode/5443517#comments",
"episode": "S21E10",
"episodeUrl": "https://titansof.tv/series/75382/episode/5443517",
"episode_id": "5443517",
"id": "19444",
"language": "en",
"leechers": 0,
"network": "FX",
"origin": "User",
"release_name": "Series.Title.S21E10.720p.HDTV.x264-KOENiG",
"resolution": "720p",
"season": "",
"season_id": 0,
"seeders": 1,
"series": "Series Title",
"series_id": "75382",
"size": 949968933,
"snatched": 0,
"source": "HDTV",
"updated_at": "2015-06-25 04:11:59",
"user_id": 0
}
],
"total": 18546
}

@ -0,0 +1,155 @@
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
using System;
using System.Linq;
using FluentAssertions;
using NzbDrone.Core.Indexers.TitansOfTv;
namespace NzbDrone.Core.Test.IndexerTests.TitansOfTvTests
{
[TestFixture]
public class TitansOfTvFixture : CoreTest<TitansOfTv>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition
{
Name = "TitansOfTV",
Settings = new TitansOfTvSettings { ApiKey = "abc", BaseUrl = "https://titansof.tv/api/torrents" }
};
}
[Test]
public void should_parse_recent_feed_from_BroadcastheNet()
{
var recentFeed = ReadAllText(@"Files/Indexers/TitansOfTv/RecentFeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Guid.Should().Be("ToTV-19445");
torrentInfo.Title.Should().Be("Series.Title.S02E04.720p.HDTV.x264-W4F");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://titansof.tv/api/torrents/19445/download?apikey=abc");
torrentInfo.InfoUrl.Should().Be("https://titansof.tv/series/287053/episode/5453241");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2015-06-25 04:13:44"));
torrentInfo.Size.Should().Be(435402993);
torrentInfo.InfoHash.Should().BeNullOrEmpty();
torrentInfo.TvRageId.Should().Be(0);
torrentInfo.MagnetUrl.Should().BeNullOrEmpty();
torrentInfo.Peers.Should().Be(2+5);
torrentInfo.Seeders.Should().Be(2);
}
private void VerifyBackOff()
{
// TODO How to detect (and implement) back-off logic.
}
[Test]
public void should_back_off_on_bad_request()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.BadRequest));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_and_report_api_key_invalid()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.Unauthorized));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_on_unknown_method()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.NotFound));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_back_off_api_limit_reached()
{
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0], System.Net.HttpStatusCode.ServiceUnavailable));
var results = Subject.FetchRecent();
results.Should().BeEmpty();
VerifyBackOff();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_replace_https_http_as_needed()
{
var recentFeed = ReadAllText(@"Files/Indexers/TitansOfTv/RecentFeed.json");
(Subject.Definition.Settings as TitansOfTvSettings).BaseUrl = "http://titansof.tv/api/torrents";
recentFeed = recentFeed.Replace("http:", "https:");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
releases.Should().HaveCount(2);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.DownloadUrl.Should().Be("http://titansof.tv/api/torrents/19445/download?apikey=abc");
torrentInfo.InfoUrl.Should().Be("http://titansof.tv/series/287053/episode/5453241");
}
}
}

@ -221,6 +221,7 @@
<Compile Include="IndexerTests\IndexerStatusServiceFixture.cs" />
<Compile Include="IndexerTests\IntegrationTests\IndexerIntegrationTests.cs" />
<Compile Include="IndexerTests\RarbgTests\RarbgFixture.cs" />
<Compile Include="IndexerTests\TitansOfTvTests\TitansOfTvFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssParserFactoryFixture.cs" />
<Compile Include="IndexerTests\TorrentRssIndexerTests\TorrentRssSettingsDetectorFixture.cs" />
<Compile Include="IndexerTests\TorznabTests\TorznabFixture.cs" />
@ -509,6 +510,9 @@
</Content>
</ItemGroup>
<ItemGroup>
<None Include="Files\Indexers\TitansOfTv\RecentFeed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Files\TestArchive.tar.gz">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

@ -0,0 +1,45 @@
using System;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.TitansOfTv
{
public class TitansOfTv : HttpIndexerBase<TitansOfTvSettings>
{
public TitansOfTv(IHttpClient httpClient, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, configService, parsingService, logger)
{
}
public override string Name
{
get
{
return "Titans of TV";
}
}
public override DownloadProtocol Protocol
{
get
{
return DownloadProtocol.Torrent;
}
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new TitansOfTvRequestGenerator() { Settings = Settings };
}
public override IParseIndexerResponse GetParser()
{
return new TitansOfTvParser();
}
public override Boolean SupportsSearch { get { return true; } }
}
}

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Indexers.TitansOfTv
{
public class Result
{
public string id { get; set; }
public string series_id { get; set; }
public string episode_id { get; set; }
public string season_id { get; set; }
public string seeders { get; set; }
public string leechers { get; set; }
public string size { get; set; }
public string snatched { get; set; }
public int user_id { get; set; }
public string anonymous { get; set; }
public string container { get; set; }
public string codec { get; set; }
public string source { get; set; }
public string resolution { get; set; }
public string origin { get; set; }
public string language { get; set; }
public string release_name { get; set; }
public string tracker_updated_at { get; set; }
public DateTime created_at { get; set; }
public DateTime updated_at { get; set; }
public string season { get; set; }
public string episode { get; set; }
public string series { get; set; }
public string network { get; set; }
public string download { get; set; }
public string episodeUrl { get; set; }
}
public class ApiResult
{
public string code { get; set; }
public int http_code { get; set; }
public int total { get; set; }
public int offset { get; set; }
public int limit { get; set; }
public List<Result> results { get; set; }
}
}

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.TitansOfTv
{
public class TitansOfTvParser : IParseIndexerResponse
{
private static readonly Regex RegexProtocol = new Regex("^https?:", RegexOptions.Compiled);
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var results = new List<ReleaseInfo>();
switch (indexerResponse.HttpResponse.StatusCode)
{
case HttpStatusCode.Unauthorized:
throw new ApiKeyException("API Key invalid or not authorized");
case HttpStatusCode.NotFound:
throw new IndexerException(indexerResponse, "Indexer API call returned NotFound, the Indexer API may have changed.");
case HttpStatusCode.ServiceUnavailable:
throw new RequestLimitReachedException("Indexer API is temporarily unavailable, try again later");
default:
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode);
}
break;
}
var content = indexerResponse.HttpResponse.Content;
var parsed = JsonConvert.DeserializeObject<ApiResult>(content);
var protocol = indexerResponse.HttpRequest.Url.Scheme + ":";
foreach (var parsedItem in parsed.results)
{
var release = new TorrentInfo();
release.Guid = String.Format("ToTV-{0}", parsedItem.id);
release.DownloadUrl = RegexProtocol.Replace(parsedItem.download, protocol);
release.InfoUrl = RegexProtocol.Replace(parsedItem.episodeUrl, protocol);
release.DownloadProtocol = DownloadProtocol.Torrent;
release.Title = parsedItem.release_name;
release.Size = Convert.ToInt64(parsedItem.size);
release.Seeders = Convert.ToInt32(parsedItem.seeders);
release.Peers = Convert.ToInt32(parsedItem.leechers) + release.Seeders;
release.PublishDate = parsedItem.created_at;
results.Add(release);
}
return results;
}
}
}

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Indexers.TitansOfTv
{
public class TitansOfTvRequestGenerator : IIndexerRequestGenerator
{
public TitansOfTvSettings Settings { get; set; }
public IList<IEnumerable<IndexerRequest>> GetRecentRequests()
{
var pageableRequests = new List<IEnumerable<IndexerRequest>>();
var innerList = new List<IndexerRequest>();
var httpRequest = BuildHttpRequest(GetBaseUrl());
innerList.Add(new IndexerRequest(httpRequest));
pageableRequests.Add(innerList);
return pageableRequests;
}
private HttpRequest BuildHttpRequest(string url)
{
var httpRequest = new HttpRequest(url, HttpAccept.Json);
httpRequest.Headers["X-Authorization"] = Settings.ApiKey;
return httpRequest;
}
public IList<IEnumerable<IndexerRequest>> GetSearchRequests(IndexerSearch.Definitions.SingleEpisodeSearchCriteria searchCriteria)
{
var url = GetBaseUrl() + "&series_id={series}&episode={episode}";
var requests = new List<IEnumerable<IndexerRequest>>();
var innerList = new List<IndexerRequest>();
requests.Add(innerList);
var httpRequest = BuildHttpRequest(url);
var episodeString = String.Format("S{0:00}E{1:00}", searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
httpRequest.AddSegment("series", searchCriteria.Series.TvdbId.ToString(CultureInfo.InvariantCulture));
httpRequest.AddSegment("episode", episodeString);
var request = new IndexerRequest(httpRequest);
innerList.Add(request);
return requests;
}
public IList<IEnumerable<IndexerRequest>> GetSearchRequests(IndexerSearch.Definitions.SeasonSearchCriteria searchCriteria)
{
var url = GetBaseUrl() + "&series_id={series}&season={season}";
var requests = new List<IEnumerable<IndexerRequest>>();
var innerList = new List<IndexerRequest>();
requests.Add(innerList);
var httpRequest = BuildHttpRequest(url);
var seasonString = String.Format("Season {0:00}", searchCriteria.SeasonNumber);
httpRequest.AddSegment("series", searchCriteria.Series.TvdbId.ToString(CultureInfo.InvariantCulture));
httpRequest.AddSegment("season", seasonString);
var request = new IndexerRequest(httpRequest);
innerList.Add(request);
httpRequest = BuildHttpRequest(url);
seasonString = String.Format("Season {0}", searchCriteria.SeasonNumber);
httpRequest.AddSegment("series", searchCriteria.Series.TvdbId.ToString(CultureInfo.InvariantCulture));
httpRequest.AddSegment("season", seasonString);
request = new IndexerRequest(httpRequest);
innerList.Add(request);
return requests;
}
public IList<IEnumerable<IndexerRequest>> GetSearchRequests(IndexerSearch.Definitions.DailyEpisodeSearchCriteria searchCriteria)
{
var url = GetBaseUrl() + "&series_id={series}&air_date={air_date}";
var requests = new List<IEnumerable<IndexerRequest>>();
var innerList = new List<IndexerRequest>();
requests.Add(innerList);
var httpRequest = BuildHttpRequest(url);
var airDate = searchCriteria.AirDate.ToString("yyyy-MM-dd");
httpRequest.AddSegment("series", searchCriteria.Series.TvdbId.ToString(CultureInfo.InvariantCulture));
httpRequest.AddSegment("air_date", airDate);
var request = new IndexerRequest(httpRequest);
innerList.Add(request);
return requests;
}
public IList<IEnumerable<IndexerRequest>> GetSearchRequests(IndexerSearch.Definitions.AnimeEpisodeSearchCriteria searchCriteria)
{
return new List<IEnumerable<IndexerRequest>>();
}
public IList<IEnumerable<IndexerRequest>> GetSearchRequests(IndexerSearch.Definitions.SpecialEpisodeSearchCriteria searchCriteria)
{
return new List<IEnumerable<IndexerRequest>>();
}
private string GetBaseUrl()
{
return Settings.BaseUrl + "?limit=100";
}
}
}

@ -0,0 +1,37 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.TitansOfTv
{
public class TitansOfTvSettingsValidator : AbstractValidator<TitansOfTvSettings>
{
public TitansOfTvSettingsValidator()
{
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class TitansOfTvSettings : IProviderConfig
{
private static readonly TitansOfTvSettingsValidator Validator = new TitansOfTvSettingsValidator();
public TitansOfTvSettings()
{
BaseUrl = "http://titansof.tv/api/torrents";
}
[FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API key", HelpText = "Enter your ToTV API key. (My Account->API->Site API Key)")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -536,6 +536,11 @@
<Compile Include="Indexers\RssSyncCommand.cs" />
<Compile Include="Indexers\RssSyncCompleteEvent.cs" />
<Compile Include="Indexers\RssSyncService.cs" />
<Compile Include="Indexers\TitansOfTv\TitansOfTv.cs" />
<Compile Include="Indexers\TitansOfTv\TitansOfTvApiResult.cs" />
<Compile Include="Indexers\TitansOfTv\TitansOfTvParser.cs" />
<Compile Include="Indexers\TitansOfTv\TitansOfTvRequestGenerator.cs" />
<Compile Include="Indexers\TitansOfTv\TitansOfTvSettings.cs" />
<Compile Include="Indexers\Torrentleech\TorrentleechRequestGenerator.cs" />
<Compile Include="Indexers\Torrentleech\Torrentleech.cs" />
<Compile Include="Indexers\Torrentleech\TorrentleechSettings.cs" />

Loading…
Cancel
Save