This commit is contained in:
Bartosz Pollok 2025-11-30 03:15:22 +01:00 committed by GitHub
commit d0ac535e37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 564 additions and 0 deletions

View file

@ -0,0 +1,212 @@
using System.Linq;
using System.Net;
using System.Text;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.Filmweb;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ImportListTests.Filmweb
{
[TestFixture]
public class FilmwebParserFixture : CoreTest<FilmwebParser>
{
private Mock<IHttpClient> _httpClient;
[SetUp]
public void Setup()
{
_httpClient = new Mock<IHttpClient>();
}
private ImportListResponse CreateResponse(string url, string content, HttpStatusCode statusCode = HttpStatusCode.OK)
{
var httpRequest = new HttpRequest(url);
var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content), statusCode);
return new ImportListResponse(new ImportListRequest(httpRequest), httpResponse);
}
private void SetupMovieInfoResponse(long entityId, string title, string originalTitle, int year)
{
var movieInfoJson = $@"{{
""id"": {entityId},
""title"": ""{title}"",
""originalTitle"": ""{originalTitle}"",
""year"": {year},
""type"": ""film"",
""subType"": ""movie"",
""posterPath"": ""/path/to/poster.jpg""
}}";
var request = new HttpRequest($"https://www.filmweb.pl/api/v1/title/{entityId}/info");
var response = new HttpResponse(request, new HttpHeader(), Encoding.UTF8.GetBytes(movieInfoJson), HttpStatusCode.OK);
_httpClient.Setup(c => c.Get(It.Is<HttpRequest>(r => r.Url.ToString().Contains($"/api/v1/title/{entityId}/info"))))
.Returns(response);
}
[Test]
public void should_parse_filmweb_want2see_list()
{
var listJson = @"[
{""entity"": 123456, ""timestamp"": 1693737600, ""level"": 5},
{""entity"": 789012, ""timestamp"": 1693824000, ""level"": 4}
]";
SetupMovieInfoResponse(123456, "Blade Runner 2049", "Blade Runner 2049", 2017);
SetupMovieInfoResponse(789012, "Dune", "Dune", 2021);
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().HaveCount(2);
result.First().Title.Should().Be("Blade Runner 2049");
result.First().Year.Should().Be(2017);
result[1].Title.Should().Be("Dune");
result[1].Year.Should().Be(2021);
}
[Test]
public void should_respect_limit_parameter()
{
var listJson = @"[
{""entity"": 111111, ""timestamp"": 1693737600, ""level"": 5},
{""entity"": 222222, ""timestamp"": 1693824000, ""level"": 4},
{""entity"": 333333, ""timestamp"": 1693910400, ""level"": 3}
]";
SetupMovieInfoResponse(111111, "Movie 1", "Movie 1", 2020);
SetupMovieInfoResponse(222222, "Movie 2", "Movie 2", 2021);
var parser = new FilmwebParser(_httpClient.Object, 2);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().HaveCount(2);
result.First().Title.Should().Be("Movie 1");
result[1].Title.Should().Be("Movie 2");
_httpClient.Verify(c => c.Get(It.Is<HttpRequest>(r => r.Url.ToString().Contains("/api/v1/title/333333/info"))), Times.Never);
}
[Test]
public void should_handle_empty_list()
{
var listJson = "[]";
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().BeEmpty();
}
[Test]
public void should_skip_movies_with_failed_info_requests()
{
var listJson = @"[
{""entity"": 123456, ""timestamp"": 1693737600, ""level"": 5},
{""entity"": 789012, ""timestamp"": 1693824000, ""level"": 4}
]";
SetupMovieInfoResponse(123456, "Working Movie", "Working Movie", 2020);
var failedRequest = new HttpRequest("https://www.filmweb.pl/api/v1/title/789012/info");
var failedResponse = new HttpResponse(failedRequest, new HttpHeader(), System.Array.Empty<byte>(), HttpStatusCode.NotFound);
_httpClient.Setup(c => c.Get(It.Is<HttpRequest>(r => r.Url.ToString().Contains("/api/v1/title/789012/info"))))
.Returns(failedResponse);
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().HaveCount(1);
result.First().Title.Should().Be("Working Movie");
}
[Test]
public void should_use_original_title_when_title_is_empty()
{
var listJson = @"[{""entity"": 123456, ""timestamp"": 1693737600, ""level"": 5}]";
var movieInfoJson = @"{
""id"": 123456,
""title"": """",
""originalTitle"": ""Original Title"",
""year"": 2020,
""type"": ""film""
}";
var request = new HttpRequest("https://www.filmweb.pl/api/v1/title/123456/info");
var response = new HttpResponse(request, new HttpHeader(), Encoding.UTF8.GetBytes(movieInfoJson), HttpStatusCode.OK);
_httpClient.Setup(c => c.Get(It.IsAny<HttpRequest>())).Returns(response);
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().HaveCount(1);
result.First().Title.Should().Be("Original Title");
}
[Test]
public void should_enforce_limit_bounds()
{
var parser1 = new FilmwebParser(_httpClient.Object, -5);
var parser2 = new FilmwebParser(_httpClient.Object, 1500);
parser1.Should().NotBeNull();
parser2.Should().NotBeNull();
}
[Test]
public void should_handle_invalid_json()
{
var invalidJson = "invalid json content";
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", invalidJson));
result.Should().BeEmpty();
}
[Test]
public void should_handle_null_movie_info()
{
var listJson = @"[{""entity"": 123456, ""timestamp"": 1693737600, ""level"": 5}]";
var request = new HttpRequest("https://www.filmweb.pl/api/v1/title/123456/info");
var response = new HttpResponse(request, new HttpHeader(), System.Array.Empty<byte>(), HttpStatusCode.InternalServerError);
_httpClient.Setup(c => c.Get(It.IsAny<HttpRequest>())).Returns(response);
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().BeEmpty();
}
[Test]
public void should_handle_malformed_entity_data()
{
var listJson = @"[
{""timestamp"": 1693737600, ""level"": 5},
{""entity"": 789012, ""timestamp"": 1693824000, ""level"": 4}
]";
SetupMovieInfoResponse(789012, "Valid Movie", "Valid Movie", 2021);
var parser = new FilmwebParser(_httpClient.Object, 100);
var result = parser.ParseResponse(CreateResponse("https://www.filmweb.pl/api/v1/user/testuser/want2see/film", listJson));
result.Should().HaveCount(1);
result.First().Title.Should().Be("Valid Movie");
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.Filmweb
{
public abstract class FilmwebImportBase<TSettings> : HttpImportListBase<TSettings>
where TSettings : FilmwebSettingsBase<TSettings>, new()
{
public override ImportListType ListType => ImportListType.Filmweb;
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(12);
protected FilmwebImportBase(IHttpClient httpClient,
IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
}
public override IParseImportListResponse GetParser()
{
return new FilmwebParser(_httpClient, Settings.Limit);
}
}
}

View file

@ -0,0 +1,156 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.ImportLists.Exceptions;
using NzbDrone.Core.ImportLists.ImportListMovies;
namespace NzbDrone.Core.ImportLists.Filmweb
{
public class FilmwebParser : IParseImportListResponse
{
private readonly IHttpClient _httpClient;
private readonly int _limit;
private ImportListResponse _importResponse;
public FilmwebParser(IHttpClient httpClient, int limit)
{
_httpClient = httpClient;
if (limit <= 0)
{
limit = 100;
}
if (limit > 1000)
{
limit = 1000;
}
_limit = limit;
}
public virtual IList<ImportListMovie> ParseResponse(ImportListResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<ImportListMovie>();
if (!PreProcess(_importResponse))
{
return movies;
}
var content = _importResponse.Content;
try
{
var movieEntities = JsonSerializer.Deserialize<List<FilmwebMovieEntity>>(content);
if (movieEntities != null)
{
var limitedEntities = movieEntities.Take(_limit).ToList();
foreach (var entity in limitedEntities)
{
try
{
var movieInfo = GetMovieInfo(entity.Entity);
if (movieInfo != null)
{
var movie = new ImportListMovie()
{
Title = !string.IsNullOrEmpty(movieInfo.Title) ? movieInfo.Title : movieInfo.OriginalTitle,
Year = movieInfo.Year
};
movies.AddIfNotNull(movie);
}
}
catch
{
continue;
}
}
}
}
catch (JsonException)
{
}
return movies;
}
private FilmwebMovieInfo GetMovieInfo(long entityId)
{
try
{
var request = new HttpRequestBuilder("https://www.filmweb.pl")
.Resource($"/api/v1/title/{entityId}/info")
.Accept(HttpAccept.Json)
.Build();
var response = _httpClient.Get(request);
if (response.StatusCode == HttpStatusCode.OK)
{
return JsonSerializer.Deserialize<FilmwebMovieInfo>(response.Content);
}
}
catch
{
}
return null;
}
protected virtual bool PreProcess(ImportListResponse importListResponse)
{
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new ImportListException(importListResponse, "Filmweb call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode);
}
return true;
}
}
public class FilmwebMovieEntity
{
[JsonPropertyName("entity")]
public long Entity { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("level")]
public int Level { get; set; }
}
public class FilmwebMovieInfo
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("originalTitle")]
public string OriginalTitle { get; set; }
[JsonPropertyName("year")]
public int Year { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("subType")]
public string SubType { get; set; }
[JsonPropertyName("posterPath")]
public string PosterPath { get; set; }
}
}

View file

@ -0,0 +1,45 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Filmweb
{
public class FilmwebSettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
where TSettings : FilmwebSettingsBase<TSettings>
{
public FilmwebSettingsBaseValidator()
{
RuleFor(c => c.Username).NotEmpty()
.WithMessage("Username is required");
RuleFor(c => c.Limit)
.GreaterThan(0)
.LessThanOrEqualTo(1000)
.WithMessage("Must be integer between 1 and 1000");
}
}
public class FilmwebSettingsBase<TSettings> : ImportListSettingsBase<TSettings>
where TSettings : FilmwebSettingsBase<TSettings>
{
private static readonly FilmwebSettingsBaseValidator<TSettings> Validator = new ();
public FilmwebSettingsBase()
{
Limit = 100;
}
public string Link => "https://www.filmweb.pl";
[FieldDefinition(1, Label = "Username", HelpText = "Filmweb username")]
public string Username { get; set; }
[FieldDefinition(98, Label = "Limit", HelpText = "Limit the number of movies to get")]
public int Limit { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
}
}
}

View file

@ -0,0 +1,33 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.Filmweb.User
{
public class FilmwebUserImport : FilmwebImportBase<FilmwebUserSettings>
{
public FilmwebUserImport(IHttpClient httpClient,
IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
}
public override string Name => "Filmweb Watchlist";
public override bool Enabled => true;
public override bool EnableAuto => false;
public override IImportListRequestGenerator GetRequestGenerator()
{
return new FilmwebUserRequestGenerator
{
Settings = Settings,
HttpClient = _httpClient,
Logger = _logger
};
}
}
}

View file

@ -0,0 +1,14 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Filmweb.User
{
public enum FilmwebUserListType
{
[FieldOption(Label = "Want to See")]
WantToSee = 0,
[FieldOption(Label = "Rated")]
Rated = 1,
[FieldOption(Label = "Favorites")]
Favorites = 2
}
}

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.ImportLists.Filmweb.User
{
public class FilmwebUserRequestGenerator : IImportListRequestGenerator
{
public FilmwebUserSettings Settings { get; set; }
public IHttpClient HttpClient { get; set; }
public Logger Logger { get; set; }
public virtual ImportListPageableRequestChain GetMovies()
{
var pageableRequests = new ImportListPageableRequestChain();
pageableRequests.Add(GetMoviesRequest());
return pageableRequests;
}
private IEnumerable<ImportListRequest> GetMoviesRequest()
{
var requestBuilder = new HttpRequestBuilder(Settings.Link.Trim())
.Accept(HttpAccept.Json);
switch (Settings.FilmwebListType)
{
case (int)FilmwebUserListType.WantToSee:
requestBuilder.Resource($"/api/v1/user/{Settings.Username.Trim()}/want2see/film");
break;
case (int)FilmwebUserListType.Rated:
requestBuilder.Resource($"/api/v1/user/{Settings.Username.Trim()}/votes/film");
break;
case (int)FilmwebUserListType.Favorites:
requestBuilder.Resource($"/api/v1/user/{Settings.Username.Trim()}/favorites/film");
break;
}
var request = new ImportListRequest(requestBuilder.Build());
yield return request;
}
}
}

View file

@ -0,0 +1,32 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Filmweb.User
{
public class FilmwebUserSettingsValidator : FilmwebSettingsBaseValidator<FilmwebUserSettings>
{
public FilmwebUserSettingsValidator()
{
RuleFor(c => c.FilmwebListType).NotNull();
}
}
public class FilmwebUserSettings : FilmwebSettingsBase<FilmwebUserSettings>
{
private static readonly FilmwebUserSettingsValidator Validator = new ();
public FilmwebUserSettings()
{
FilmwebListType = (int)FilmwebUserListType.WantToSee;
}
[FieldDefinition(2, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(FilmwebUserListType), HelpText = "Type of list to import from Filmweb")]
public int FilmwebListType { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View file

@ -7,6 +7,7 @@ public enum ImportListType
Trakt,
Plex,
Simkl,
Filmweb,
Other,
Advanced
}