Skip to content

Commit

Permalink
New: Plex Watchlist RSS support
Browse files Browse the repository at this point in the history
(cherry picked from commit 6d88a98282d1441f903d567470a9f1ce6ba0b52f)
  • Loading branch information
markus101 authored and mynameisbogdan committed May 15, 2023
1 parent f14482c commit 588c8fb
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImport.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
46 changes: 46 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportParser.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
27 changes: 27 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/Plex/PlexRssImportSettings.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
43 changes: 43 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/RssImportBase.cs
Original file line number Diff line number Diff line change
@@ -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
};
}
}
}
142 changes: 142 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/RssImportBaseParser.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
28 changes: 28 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/RssImportBaseSettings.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
26 changes: 26 additions & 0 deletions src/NzbDrone.Core/ImportLists/Rss/RssImportRequestGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

0 comments on commit 588c8fb

Please sign in to comment.