Swap Orpheus to API key (#946)

* New: Orpheus uses API key instead of user/pass

* fixup! New: Orpheus uses API key instead of user/pass

Co-authored-by: Qstick <qstick@gmail.com>
pull/1205/head
ta264 2 years ago committed by GitHub
parent bd0115931f
commit 4f4c011436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,63 @@
using System.Data;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(22)]
public class orpheus_api : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToRedactedApi);
}
private void MigrateToRedactedApi(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Indexers\" WHERE \"Implementation\" = 'Orpheus'";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = Json.Deserialize<JObject>(settings);
// Remove username
if (jsonObject.ContainsKey("username"))
{
jsonObject.Remove("username");
}
// Remove password
if (jsonObject.ContainsKey("password"))
{
jsonObject.Remove("password");
}
// write new json back to db, switch to new ConfigContract, and disable the indexer
settings = jsonObject.ToJson();
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE \"Indexers\" SET \"Settings\" = ?, \"ConfigContract\" = ?, \"Enable\" = 0 WHERE \"Id\" = ?";
updateCmd.AddParameter(settings);
updateCmd.AddParameter("OrpheusSettings");
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}
}

@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public string AuthKey;
public string PassKey;
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use Freeleech Token")]
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Use Freeleech Token", HelpText = "Use freeleech tokens when available")]
public bool UseFreeleechToken { get; set; }
}
}

@ -1,25 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class Orpheus : Gazelle.Gazelle
public class Orpheus : TorrentIndexerBase<OrpheusSettings>
{
public override string Name => "Orpheus";
public override string[] IndexerUrls => new string[] { "https://orpheus.network/" };
public override string Description => "Orpheus (APOLLO) is a Private Torrent Tracker for MUSIC";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
public override bool SupportsRedirect => true;
public Orpheus(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
protected override IndexerCapabilities SetCapabilities()
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new OrpheusRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient };
}
public override IParseIndexerResponse GetParser()
{
return new OrpheusParser(Settings, Capabilities.Categories);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
@ -44,23 +70,252 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
public override IParseIndexerResponse GetParser()
public override async Task<byte[]> Download(Uri link)
{
var request = new HttpRequestBuilder(link.AbsoluteUri)
.SetHeader("Authorization", $"token {Settings.Apikey}")
.Build();
var downloadBytes = Array.Empty<byte>();
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
if (downloadBytes.Length >= 1
&& downloadBytes[0] != 'd' // simple test for torrent vs HTML content
&& link.Query.Contains("usetoken=1"))
{
var html = Encoding.GetString(downloadBytes);
if (html.Contains("You do not have any freeleech tokens left.")
|| html.Contains("You do not have enough freeleech tokens")
|| html.Contains("This torrent is too large.")
|| html.Contains("You cannot use tokens here"))
{
// download again without usetoken
request.Url = new HttpUri(link.ToString().Replace("&usetoken=1", ""));
response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
}
}
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Download failed");
}
return downloadBytes;
}
}
public class OrpheusRequestGenerator : IIndexerRequestGenerator
{
public OrpheusSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public IIndexerHttpClient HttpClient { get; set; }
public OrpheusRequestGenerator()
{
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
return new OrpheusParser(Settings, Capabilities);
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(string.Format("&artistname={0}&groupname={1}", searchCriteria.Artist, searchCriteria.Album)));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetRequest(searchCriteria.SanitizedSearchTerm));
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var req = RequestBuilder()
.Resource($"ajax.php?action=browse&searchstr={searchParameters}")
.Build();
yield return new IndexerRequest(req);
}
private HttpRequestBuilder RequestBuilder()
{
return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}")
.Accept(HttpAccept.Json)
.SetHeader("Authorization", $"token {Settings.Apikey}");
}
}
public class OrpheusParser : GazelleParser
public class OrpheusParser : IParseIndexerResponse
{
public OrpheusParser(GazelleSettings settings, IndexerCapabilities capabilities)
: base(settings, capabilities)
private readonly OrpheusSettings _settings;
private readonly IndexerCapabilitiesCategories _categories;
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public OrpheusParser(OrpheusSettings settings, IndexerCapabilitiesCategories categories)
{
_settings = settings;
_categories = categories;
}
protected override string GetDownloadUrl(int torrentId)
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
}
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
{
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
}
var jsonResponse = new HttpResponse<GazelleResponse>(indexerResponse.HttpResponse);
if (jsonResponse.Resource.Status != "success" ||
string.IsNullOrWhiteSpace(jsonResponse.Resource.Status) ||
jsonResponse.Resource.Response == null)
{
return torrentInfos;
}
foreach (var result in jsonResponse.Resource.Response.Results)
{
if (result.Torrents != null)
{
foreach (var torrent in result.Torrents)
{
var id = torrent.TorrentId;
var artist = WebUtility.HtmlDecode(result.Artist);
var album = WebUtility.HtmlDecode(result.GroupName);
var title = $"{result.Artist} - {result.GroupName} ({result.GroupYear}) [{torrent.Format} {torrent.Encoding}] [{torrent.Media}]";
if (torrent.HasCue)
{
title += " [Cue]";
}
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
{
Guid = infoUrl,
// Splice Title from info to avoid calling API again for every torrent.
Title = WebUtility.HtmlDecode(title),
Container = torrent.Encoding,
Codec = torrent.Format,
Size = long.Parse(torrent.Size),
DownloadUrl = GetDownloadUrl(id, torrent.CanUseToken),
InfoUrl = infoUrl,
Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.Time.ToUniversalTime(),
Scene = torrent.Scene,
Freeleech = torrent.IsFreeLeech || torrent.IsPersonalFreeLeech,
Files = torrent.FileCount,
Grabs = torrent.Snatches,
DownloadVolumeFactor = torrent.IsFreeLeech || torrent.IsNeutralLeech || torrent.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = torrent.IsNeutralLeech ? 0 : 1
};
var category = torrent.Category;
if (category == null || category.Contains("Select Category"))
{
release.Categories = _categories.MapTrackerCatToNewznab("1");
}
else
{
release.Categories = _categories.MapTrackerCatDescToNewznab(category);
}
torrentInfos.Add(release);
}
}
// Non-Audio files are formatted a little differently (1:1 for group and torrents)
else
{
var id = result.TorrentId;
var infoUrl = GetInfoUrl(result.GroupId, id);
GazelleInfo release = new GazelleInfo()
{
Guid = infoUrl,
Title = WebUtility.HtmlDecode(result.GroupName),
Size = long.Parse(result.Size),
DownloadUrl = GetDownloadUrl(id, result.CanUseToken),
InfoUrl = infoUrl,
Seeders = int.Parse(result.Seeders),
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
Freeleech = result.IsFreeLeech || result.IsPersonalFreeLeech,
Files = result.FileCount,
Grabs = result.Snatches,
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1
};
var category = result.Category;
if (category == null || category.Contains("Select Category"))
{
release.Categories = _categories.MapTrackerCatToNewznab("1");
}
else
{
release.Categories = _categories.MapTrackerCatDescToNewznab(category);
}
torrentInfos.Add(release);
}
}
// order by date
return
torrentInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
private string GetDownloadUrl(int torrentId, bool canUseToken)
{
// AuthKey is required but not checked, just pass in a dummy variable
// to avoid having to track authkey, which is randomly cycled
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.CombinePath("/ajax.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId);
@ -72,5 +327,45 @@ namespace NzbDrone.Core.Indexers.Definitions
return url.FullUri;
}
private string GetInfoUrl(string groupId, int torrentId)
{
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("id", groupId)
.AddQueryParam("torrentid", torrentId);
return url.FullUri;
}
}
public class OrpheusSettingsValidator : AbstractValidator<OrpheusSettings>
{
public OrpheusSettingsValidator()
{
RuleFor(c => c.Apikey).NotEmpty();
}
}
public class OrpheusSettings : NoAuthTorrentBaseSettings
{
private static readonly OrpheusSettingsValidator Validator = new OrpheusSettingsValidator();
public OrpheusSettings()
{
Apikey = "";
UseFreeleechToken = false;
}
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", Privacy = PrivacyLevel.ApiKey)]
public string Apikey { get; set; }
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
public bool UseFreeleechToken { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -27,8 +27,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string Name => "Redacted";
public override string[] IndexerUrls => new string[] { "https://redacted.ch/" };
public override string Description => "REDActed (Aka.PassTheHeadPhones) is one of the most well-known music trackers.";
public override string Language => "en-US";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@ -82,10 +80,26 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
protected override async Task Test(List<ValidationFailure> failures)
public override async Task<byte[]> Download(Uri link)
{
((RedactedRequestGenerator)GetRequestGenerator()).FetchPasskey();
await base.Test(failures);
var request = new HttpRequestBuilder(link.AbsoluteUri)
.SetHeader("Authorization", Settings.Apikey)
.Build();
var downloadBytes = Array.Empty<byte>();
try
{
var response = await _httpClient.ExecuteProxiedAsync(request, Definition);
downloadBytes = response.ResponseData;
}
catch (Exception)
{
_indexerStatusService.RecordFailure(Definition.Id);
_logger.Error("Download failed");
}
return downloadBytes;
}
}
@ -138,24 +152,6 @@ namespace NzbDrone.Core.Indexers.Definitions
return pageableRequests;
}
public void FetchPasskey()
{
// GET on index for the passkey
var request = RequestBuilder().Resource("ajax.php?action=index").Build();
var indexResponse = HttpClient.Execute(request);
var index = Json.Deserialize<GazelleAuthResponse>(indexResponse.Content);
if (index == null ||
string.IsNullOrWhiteSpace(index.Status) ||
index.Status != "success" ||
string.IsNullOrWhiteSpace(index.Response.Passkey))
{
throw new Exception("Failed to authenticate with Redacted.");
}
// Set passkey on settings so it can be used to generate the download URL
Settings.Passkey = index.Response.Passkey;
}
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var req = RequestBuilder()
@ -311,11 +307,9 @@ namespace NzbDrone.Core.Indexers.Definitions
// AuthKey is required but not checked, just pass in a dummy variable
// to avoid having to track authkey, which is randomly cycled
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.CombinePath("/ajax.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId)
.AddQueryParam("authkey", "prowlarr")
.AddQueryParam("torrent_pass", _settings.Passkey)
.AddQueryParam("usetoken", (_settings.UseFreeleechToken && canUseToken) ? 1 : 0);
return url.FullUri;
@ -347,7 +341,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public RedactedSettings()
{
Apikey = "";
Passkey = "";
UseFreeleechToken = false;
}
@ -357,8 +350,6 @@ namespace NzbDrone.Core.Indexers.Definitions
[FieldDefinition(3, Label = "Use Freeleech Tokens", HelpText = "Use freeleech tokens when available", Type = FieldType.Checkbox)]
public bool UseFreeleechToken { get; set; }
public string Passkey { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

Loading…
Cancel
Save