mirror of https://github.com/Radarr/Radarr
New: Plex Watchlist RSS support
(cherry picked from commit 6d88a98282d1441f903d567470a9f1ce6ba0b52f)
This commit is contained in:
parent
f14482cb59
commit
588c8fb074
|
@ -0,0 +1,27 @@
|
|||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss.Plex
|
||||
{
|
||||
public class PlexRssImport : RssImportBase<PlexRssImportSettings>
|
||||
{
|
||||
public override string Name => "Plex Watchlist RSS";
|
||||
public override ImportListType ListType => ImportListType.Plex;
|
||||
|
||||
public PlexRssImport(IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new PlexRssImportParser(_logger);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss.Plex
|
||||
{
|
||||
public class PlexRssImportParser : RssImportBaseParser
|
||||
{
|
||||
public PlexRssImportParser(Logger logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ImportListMovie ProcessItem(XElement item)
|
||||
{
|
||||
var category = item.TryGetValue("category");
|
||||
|
||||
if (category != "movie")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var info = new ImportListMovie
|
||||
{
|
||||
Title = item.TryGetValue("title", "Unknown")
|
||||
};
|
||||
|
||||
var guid = item.TryGetValue("guid", string.Empty);
|
||||
|
||||
if (guid.IsNotNullOrWhiteSpace() && guid.StartsWith("imdb://"))
|
||||
{
|
||||
info.ImdbId = Parser.Parser.ParseImdbId(guid.Replace("imdb://", ""));
|
||||
}
|
||||
|
||||
if (info.ImdbId.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new UnsupportedFeedException("Each item in the RSS feed must have a guid element with a IMDB ID");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss.Plex
|
||||
{
|
||||
public class PlexRssImportSettingsValidator : AbstractValidator<PlexRssImportSettings>
|
||||
{
|
||||
public PlexRssImportSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Url).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class PlexRssImportSettings : RssImportBaseSettings
|
||||
{
|
||||
private PlexRssImportSettingsValidator Validator => new ();
|
||||
|
||||
[FieldDefinition(0, Label = "Url", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")]
|
||||
public override string Url { get; set; }
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss
|
||||
{
|
||||
public abstract class RssImportBase<TSettings> : HttpImportListBase<TSettings>
|
||||
where TSettings : RssImportBaseSettings, new()
|
||||
{
|
||||
public override bool Enabled => true;
|
||||
public override bool EnableAuto => false;
|
||||
|
||||
public RssImportBase(IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override ImportListFetchResult Fetch()
|
||||
{
|
||||
var generator = GetRequestGenerator();
|
||||
|
||||
return FetchMovies(generator.GetMovies());
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new RssImportBaseParser(_logger);
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new RssImportRequestGenerator
|
||||
{
|
||||
Settings = Settings
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.ImportLists.Exceptions;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss
|
||||
{
|
||||
public class RssImportBaseParser : IParseImportListResponse
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RssImportBaseParser(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public virtual IList<ImportListMovie> ParseResponse(ImportListResponse importResponse)
|
||||
{
|
||||
var movies = new List<ImportListMovie>();
|
||||
|
||||
if (!PreProcess(importResponse))
|
||||
{
|
||||
return movies;
|
||||
}
|
||||
|
||||
var document = LoadXmlDocument(importResponse);
|
||||
var items = GetItems(document).ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
try
|
||||
{
|
||||
var itemInfo = ProcessItem(item);
|
||||
|
||||
movies.AddIfNotNull(itemInfo);
|
||||
}
|
||||
catch (UnsupportedFeedException itemEx)
|
||||
{
|
||||
itemEx.WithData("FeedUrl", importResponse.Request.Url);
|
||||
itemEx.WithData("ItemTitle", item.Title());
|
||||
throw;
|
||||
}
|
||||
catch (Exception itemEx)
|
||||
{
|
||||
itemEx.WithData("FeedUrl", importResponse.Request.Url);
|
||||
itemEx.WithData("ItemTitle", item.Title());
|
||||
_logger.Error(itemEx, "An error occurred while processing feed item from {0}", importResponse.Request.Url);
|
||||
}
|
||||
}
|
||||
|
||||
return movies;
|
||||
}
|
||||
|
||||
protected virtual bool PreProcess(ImportListResponse importListResponse)
|
||||
{
|
||||
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new ImportListException(importListResponse, "Request resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/xml") &&
|
||||
importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/xml"))
|
||||
{
|
||||
throw new ImportListException(importListResponse, "Request responded with html content. Site is likely blocked or unavailable.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual XDocument LoadXmlDocument(ImportListResponse importListResponse)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = XmlCleaner.ReplaceEntities(importListResponse.Content);
|
||||
content = XmlCleaner.ReplaceUnicode(content);
|
||||
|
||||
using var xmlTextReader = XmlReader.Create(new StringReader(content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true });
|
||||
|
||||
return XDocument.Load(xmlTextReader);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
var contentSample = importListResponse.Content.Substring(0, Math.Min(importListResponse.Content.Length, 512));
|
||||
_logger.Debug("Truncated response content (originally {0} characters): {1}", importListResponse.Content.Length, contentSample);
|
||||
|
||||
ex.WithData(importListResponse.HttpResponse);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerable<XElement> GetItems(XDocument document)
|
||||
{
|
||||
var root = document.Root;
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
return Enumerable.Empty<XElement>();
|
||||
}
|
||||
|
||||
var channel = root.Element("channel");
|
||||
|
||||
if (channel == null)
|
||||
{
|
||||
return Enumerable.Empty<XElement>();
|
||||
}
|
||||
|
||||
return channel.Elements("item");
|
||||
}
|
||||
|
||||
protected virtual ImportListMovie ProcessItem(XElement item)
|
||||
{
|
||||
var info = new ImportListMovie
|
||||
{
|
||||
Title = item.TryGetValue("title", "Unknown")
|
||||
};
|
||||
|
||||
var guid = item.TryGetValue("guid");
|
||||
|
||||
if (guid != null && int.TryParse(guid, out var tmdbId))
|
||||
{
|
||||
info.TmdbId = tmdbId;
|
||||
}
|
||||
|
||||
if (info.TmdbId == 0)
|
||||
{
|
||||
throw new UnsupportedFeedException("Each item in the RSS feed must have a guid element with a TMDB ID");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss
|
||||
{
|
||||
public class RssImportSettingsValidator : AbstractValidator<RssImportBaseSettings>
|
||||
{
|
||||
public RssImportSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Url).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class RssImportBaseSettings : IProviderConfig
|
||||
{
|
||||
private RssImportSettingsValidator Validator => new ();
|
||||
|
||||
[FieldDefinition(0, Label = "Url", Type = FieldType.Textbox)]
|
||||
public virtual string Url { get; set; }
|
||||
|
||||
public virtual NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Rss
|
||||
{
|
||||
public class RssImportRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
public RssImportBaseSettings Settings { get; set; }
|
||||
|
||||
public virtual ImportListPageableRequestChain GetMovies()
|
||||
{
|
||||
var pageableRequests = new ImportListPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetMoviesRequest());
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetMoviesRequest()
|
||||
{
|
||||
var request = new ImportListRequest(Settings.Url, HttpAccept.Rss);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue