Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[enhancement]: support Torznab o=JSON #15036

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Jackett.Common/Models/DTO/TorznabRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class TorznabRequest
public string imdbid { get; set; }
public string ep { get; set; }
public string t { get; set; }
public string o { get; set; }
public string extended { get; set; }
public string limit { get; set; }
public string offset { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions src/Jackett.Common/Models/TorznabQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class TorznabQuery

public bool IsTest { get; set; }

public bool IsJson { get; set; }

public string ImdbIDShort => ImdbID?.TrimStart('t');

protected string[] QueryStringParts;
Expand Down
74 changes: 61 additions & 13 deletions src/Jackett.Server/Controllers/ResultsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using NLog;

namespace Jackett.Server.Controllers
Expand Down Expand Up @@ -333,9 +334,16 @@ public async Task<IActionResult> Results([FromQuery] ApiSearch requestt)
[HttpGet]
public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
{
if (string.Equals(request.o, "json", StringComparison.InvariantCultureIgnoreCase))
{
CurrentQuery.IsJson = true;
}

if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
{
return Content(CurrentIndexer.TorznabCaps.ToXml(), "application/rss+xml", Encoding.UTF8);
return CurrentQuery.IsJson ?
(IActionResult)Json(CurrentIndexer.TorznabCaps) :
Content(CurrentIndexer.TorznabCaps.ToXml(), "application/rss+xml", Encoding.UTF8);
}

// indexers - returns a list of all included indexers (meta indexers only)
Expand All @@ -344,7 +352,7 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
if (!(CurrentIndexer is BaseMetaIndexer)) // shouldn't be needed because CanHandleQuery should return false
{
logger.Warn($"A search request with t=indexers from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} isn't a meta indexer.");
return GetErrorXML(203, "Function Not Available: this isn't a meta indexer");
return GetError(203, "Function Not Available: this isn't a meta indexer");
}
var CurrentBaseMetaIndexer = (BaseMetaIndexer)CurrentIndexer;
var indexers = CurrentBaseMetaIndexer.Indexers;
Expand All @@ -353,6 +361,24 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
else if (string.Equals(request.configured, "false", StringComparison.InvariantCultureIgnoreCase))
indexers = indexers.Where(i => !i.IsConfigured);

if (CurrentQuery.IsJson)
{
return Json(new JObject
{
["indexers"] = JArray.FromObject(indexers.Select(i => new
{
id = i.Id,
configured = i.IsConfigured,
title = i.Name,
description = i.Description,
link = i.SiteLink,
language = i.Language,
type = i.Type,
caps = i.TorznabCaps
}))
});
}

var xdoc = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
new XElement("indexers",
Expand Down Expand Up @@ -389,19 +415,19 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
if (CurrentQuery.ImdbID == null)
{
logger.Warn($"A search request from {Request.HttpContext.Connection.RemoteIpAddress} was made with an invalid imdbid.");
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
return GetError(201, "Incorrect parameter: invalid imdbid format");
}

if (CurrentQuery.IsMovieSearch && !CurrentIndexer.TorznabCaps.MovieSearchImdbAvailable)
{
logger.Warn($"A search request with imdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} doesn't support it.");
return GetErrorXML(203, "Function Not Available: imdbid is not supported for movie search by this indexer");
return GetError(203, "Function Not Available: imdbid is not supported for movie search by this indexer");
}

if (CurrentQuery.IsTVSearch && !CurrentIndexer.TorznabCaps.TvSearchImdbAvailable)
{
logger.Warn($"A search request with imdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} doesn't support it.");
return GetErrorXML(203, "Function Not Available: imdbid is not supported for TV search by this indexer");
return GetError(203, "Function Not Available: imdbid is not supported for TV search by this indexer");
}
}

Expand All @@ -410,13 +436,13 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
if (CurrentQuery.IsMovieSearch && !CurrentIndexer.TorznabCaps.MovieSearchTmdbAvailable)
{
logger.Warn($"A search request with tmdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} doesn't support it.");
return GetErrorXML(203, "Function Not Available: tmdbid is not supported for movie search by this indexer");
return GetError(203, "Function Not Available: tmdbid is not supported for movie search by this indexer");
}

if (CurrentQuery.IsTVSearch && !CurrentIndexer.TorznabCaps.TvSearchTmdbAvailable)
{
logger.Warn($"A search request with tmdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} doesn't support it.");
return GetErrorXML(203, "Function Not Available: tmdbid is not supported for TV search by this indexer");
return GetError(203, "Function Not Available: tmdbid is not supported for TV search by this indexer");
}
}

Expand All @@ -425,7 +451,7 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
if (CurrentQuery.IsTVSearch && !CurrentIndexer.TorznabCaps.TvSearchAvailable)
{
logger.Warn($"A search request with tvdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.Name} doesn't support it.");
return GetErrorXML(203, "Function Not Available: tvdbid is not supported for movie search by this indexer");
return GetError(203, "Function Not Available: tvdbid is not supported for movie search by this indexer");
}
}

Expand Down Expand Up @@ -459,6 +485,11 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
else
logger.Info($"Torznab search in {CurrentIndexer.Name} for {CurrentQuery.GetQueryString()} => Found {result.Releases.Count()} releases{cacheStr} [{stopwatch.ElapsedMilliseconds:0}ms]");

if (CurrentQuery.IsJson)
{
return Json(resultPage);
}

var xml = resultPage.ToXml(new Uri(serverUrl));

// Force the return as XML
Expand All @@ -479,25 +510,29 @@ public async Task<IActionResult> Torznab([FromQuery] TorznabRequest request)
}
}

return GetErrorXML(900, ex.Message, StatusCodes.Status429TooManyRequests);
return GetError(900, ex.Message, StatusCodes.Status429TooManyRequests);
}
catch (Exception e)
{
logger.Error(e);
return GetErrorXML(900, e.ToString());
return GetError(900, e.ToString());
}
}

[Route("[action]/{ignored?}")]
public IActionResult GetErrorXML(int code, string description, int statusCode = StatusCodes.Status400BadRequest)
public IActionResult GetError(int code, string description, int statusCode = StatusCodes.Status400BadRequest)
{
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse("application/xml");
var mediaTypeHeaderValue = CurrentQuery.IsJson ?
MediaTypeHeaderValue.Parse("application/json") :
MediaTypeHeaderValue.Parse("application/xml");
mediaTypeHeaderValue.Encoding = Encoding.UTF8;

return new ContentResult
{
StatusCode = statusCode,
Content = CreateErrorXML(code, description),
Content = CurrentQuery.IsJson ?
CreateErrorJson(code, description) :
CreateErrorXML(code, description),
ContentType = mediaTypeHeaderValue.ToString()
};
}
Expand All @@ -514,6 +549,19 @@ public static string CreateErrorXML(int code, string description)
return xdoc.Declaration + Environment.NewLine + xdoc;
}

public static string CreateErrorJson(int code, string description)
{
return new JObject
{
{ "error", new JObject
{
{ "code", code },
{ "description", description }
}
}
}.ToString();
}

public static IActionResult GetErrorActionResult(RouteData routeData, HttpStatusCode status, int torznabCode, string description)
{
var isTorznab = routeData.Values["action"].ToString().Equals("torznab", StringComparison.OrdinalIgnoreCase);
Expand Down