parent
944f420270
commit
7f221c7834
@ -1,22 +1,9 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
*text eol=lf
|
||||
|
||||
# Explicitly set bash scripts to have unix endings
|
||||
*.sh text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
#*.sln merge=union
|
||||
#*.csproj merge=union
|
||||
#*.vbproj merge=union
|
||||
#*.fsproj merge=union
|
||||
#*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
*.sln merge=union
|
||||
|
@ -1,4 +0,0 @@
|
||||
[submodule "src/ExternalModules/CurlSharp"]
|
||||
path = src/ExternalModules/CurlSharp
|
||||
url = https://github.com/Sonarr/CurlSharp.git
|
||||
branch = master
|
@ -1 +0,0 @@
|
||||
Subproject commit cfdbbbd9c6b9612c2756245049a8234ce87dc576
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<dllmap dll="libcurl.dll" target="libcurl.so.4" />
|
||||
<dllmap os="osx" dll="libcurl.dll" target="libcurl.4.dylib"/>
|
||||
<!--<dllmap os="freebsd" dll="libcurl.dll" target="libcurl.so.4" />-->
|
||||
<!--<dllmap os="solaris" dll="libcurl.dll" target="libcurl.so.4" />-->
|
||||
</configuration>
|
@ -1,342 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CurlSharp;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http.Proxy;
|
||||
|
||||
namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
public class CurlHttpDispatcher : IHttpDispatcher
|
||||
{
|
||||
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
|
||||
private readonly IUserAgentBuilder _userAgentBuilder;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private const string _caBundleFileName = "curl-ca-bundle.crt";
|
||||
private static readonly string _caBundleFilePath;
|
||||
|
||||
static CurlHttpDispatcher()
|
||||
{
|
||||
if (Assembly.GetExecutingAssembly().Location.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_caBundleFilePath = Path.Combine(Assembly.GetExecutingAssembly().Location, "..", _caBundleFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_caBundleFilePath = _caBundleFileName;
|
||||
}
|
||||
}
|
||||
|
||||
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger)
|
||||
{
|
||||
_proxySettingsProvider = proxySettingsProvider;
|
||||
_userAgentBuilder = userAgentBuilder;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool CheckAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
return CurlGlobalHandle.Instance.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex, "Initializing curl failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||
{
|
||||
if (!CheckAvailability())
|
||||
{
|
||||
throw new ApplicationException("Curl failed to initialize.");
|
||||
}
|
||||
|
||||
lock (CurlGlobalHandle.Instance)
|
||||
{
|
||||
Stream responseStream = new MemoryStream();
|
||||
Stream headerStream = new MemoryStream();
|
||||
|
||||
using (var curlEasy = new CurlEasy())
|
||||
{
|
||||
curlEasy.AutoReferer = false;
|
||||
curlEasy.WriteFunction = (b, s, n, o) =>
|
||||
{
|
||||
responseStream.Write(b, 0, s * n);
|
||||
return s * n;
|
||||
};
|
||||
curlEasy.HeaderFunction = (b, s, n, o) =>
|
||||
{
|
||||
headerStream.Write(b, 0, s * n);
|
||||
return s * n;
|
||||
};
|
||||
|
||||
AddProxy(curlEasy, request);
|
||||
|
||||
curlEasy.Url = request.Url.FullUri;
|
||||
|
||||
switch (request.Method)
|
||||
{
|
||||
case HttpMethod.GET:
|
||||
curlEasy.HttpGet = true;
|
||||
break;
|
||||
|
||||
case HttpMethod.POST:
|
||||
curlEasy.Post = true;
|
||||
break;
|
||||
|
||||
case HttpMethod.PUT:
|
||||
curlEasy.Put = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"HttpCurl method {request.Method} not supported");
|
||||
}
|
||||
curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
|
||||
curlEasy.FollowLocation = false;
|
||||
|
||||
if (request.RequestTimeout != TimeSpan.Zero)
|
||||
{
|
||||
curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds);
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
curlEasy.CaInfo = _caBundleFilePath;
|
||||
}
|
||||
|
||||
if (cookies != null)
|
||||
{
|
||||
curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url);
|
||||
}
|
||||
|
||||
if (request.ContentData != null)
|
||||
{
|
||||
curlEasy.PostFieldSize = request.ContentData.Length;
|
||||
curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v)));
|
||||
}
|
||||
|
||||
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
|
||||
using (var httpRequestHeaders = SerializeHeaders(request))
|
||||
{
|
||||
curlEasy.HttpHeader = httpRequestHeaders;
|
||||
|
||||
var result = curlEasy.Perform();
|
||||
|
||||
if (result != CurlCode.Ok)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case CurlCode.SslCaCert:
|
||||
case (CurlCode)77:
|
||||
throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url));
|
||||
default:
|
||||
throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream);
|
||||
var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection);
|
||||
|
||||
var httpHeader = new HttpHeader(webHeaderCollection);
|
||||
|
||||
return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddProxy(CurlEasy curlEasy, HttpRequest request)
|
||||
{
|
||||
var proxySettings = _proxySettingsProvider.GetProxySettings(request);
|
||||
if (proxySettings != null)
|
||||
|
||||
{
|
||||
switch (proxySettings.Type)
|
||||
{
|
||||
case ProxyType.Http:
|
||||
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Http);
|
||||
curlEasy.SetOpt(CurlOption.ProxyAuth, CurlHttpAuth.Basic);
|
||||
curlEasy.SetOpt(CurlOption.ProxyUserPwd, proxySettings.Username + ":" + proxySettings.Password.ToString());
|
||||
break;
|
||||
case ProxyType.Socks4:
|
||||
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks4);
|
||||
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
|
||||
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
|
||||
break;
|
||||
case ProxyType.Socks5:
|
||||
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks5);
|
||||
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
|
||||
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
|
||||
break;
|
||||
}
|
||||
curlEasy.SetOpt(CurlOption.Proxy, proxySettings.Host + ":" + proxySettings.Port.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private CurlSlist SerializeHeaders(HttpRequest request)
|
||||
{
|
||||
if (!request.Headers.ContainsKey("Accept-Encoding"))
|
||||
{
|
||||
request.Headers.Add("Accept-Encoding", "gzip");
|
||||
}
|
||||
|
||||
if (request.Headers.ContentType == null)
|
||||
{
|
||||
request.Headers.ContentType = string.Empty;
|
||||
}
|
||||
|
||||
var curlHeaders = new CurlSlist();
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
curlHeaders.Append(header.Key + ": " + header.Value.ToString());
|
||||
}
|
||||
|
||||
return curlHeaders;
|
||||
}
|
||||
|
||||
private WebHeaderCollection ProcessHeaderStream(HttpRequest request, CookieContainer cookies, Stream headerStream)
|
||||
{
|
||||
headerStream.Position = 0;
|
||||
var headerData = headerStream.ToBytes();
|
||||
var headerString = Encoding.ASCII.GetString(headerData);
|
||||
|
||||
var webHeaderCollection = new WebHeaderCollection();
|
||||
|
||||
// following a redirect we could have two sets of headers, so only process the last one
|
||||
foreach (var header in headerString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Reverse())
|
||||
{
|
||||
if (!header.Contains(":")) break;
|
||||
webHeaderCollection.Add(header);
|
||||
}
|
||||
|
||||
var setCookie = webHeaderCollection.Get("Set-Cookie");
|
||||
if (setCookie != null && setCookie.Length > 0 && cookies != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cookies.SetCookies((Uri)request.Url, FixSetCookieHeader(setCookie));
|
||||
}
|
||||
catch (CookieException ex)
|
||||
{
|
||||
_logger.Debug("Rejected cookie {0}: {1}", ex.InnerException.Message, setCookie);
|
||||
}
|
||||
}
|
||||
|
||||
return webHeaderCollection;
|
||||
}
|
||||
|
||||
private string FixSetCookieHeader(string setCookie)
|
||||
{
|
||||
// fix up the date if it was malformed
|
||||
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
|
||||
{
|
||||
string shortFormat = "ddd, dd-MMM-yy HH:mm:ss";
|
||||
string longFormat = "ddd, dd-MMM-yyyy HH:mm:ss";
|
||||
DateTime dt;
|
||||
if (DateTime.TryParseExact(match.Groups[2].Value, longFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
|
||||
DateTime.TryParseExact(match.Groups[2].Value, shortFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
|
||||
DateTime.TryParse(match.Groups[2].Value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt))
|
||||
return match.Groups[1].Value + dt.ToUniversalTime().ToString(longFormat, CultureInfo.InvariantCulture) + " GMT";
|
||||
else
|
||||
return match.Value;
|
||||
});
|
||||
return setCookieClean;
|
||||
}
|
||||
|
||||
private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection)
|
||||
{
|
||||
byte[] bytes = null;
|
||||
responseStream.Position = 0;
|
||||
|
||||
if (responseStream.Length != 0)
|
||||
{
|
||||
var encoding = webHeaderCollection["Content-Encoding"];
|
||||
if (encoding != null)
|
||||
{
|
||||
if (encoding.IndexOf("gzip") != -1)
|
||||
{
|
||||
using (var zipStream = new GZipStream(responseStream, CompressionMode.Decompress))
|
||||
{
|
||||
bytes = zipStream.ToBytes();
|
||||
}
|
||||
|
||||
webHeaderCollection.Remove("Content-Encoding");
|
||||
}
|
||||
else if (encoding.IndexOf("deflate") != -1)
|
||||
{
|
||||
using (var deflateStream = new DeflateStream(responseStream, CompressionMode.Decompress))
|
||||
{
|
||||
bytes = deflateStream.ToBytes();
|
||||
}
|
||||
|
||||
webHeaderCollection.Remove("Content-Encoding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes == null) bytes = responseStream.ToBytes();
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CurlGlobalHandle : SafeHandle
|
||||
{
|
||||
public static readonly CurlGlobalHandle Instance = new CurlGlobalHandle();
|
||||
|
||||
private bool _initialized;
|
||||
private bool _available;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private CurlGlobalHandle()
|
||||
: base(IntPtr.Zero, true)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool Initialize()
|
||||
{
|
||||
lock (CurlGlobalHandle.Instance)
|
||||
{
|
||||
if (_initialized)
|
||||
return _available;
|
||||
|
||||
_initialized = true;
|
||||
_available = Curl.GlobalInit(CurlInitFlag.All) == CurlCode.Ok;
|
||||
|
||||
return _available;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
if (_initialized && _available)
|
||||
{
|
||||
Curl.GlobalCleanup();
|
||||
_available = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool IsInvalid => !_initialized || !_available;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.Common.Http.Dispatchers
|
||||
{
|
||||
public class FallbackHttpDispatcher : IHttpDispatcher
|
||||
{
|
||||
private readonly ManagedHttpDispatcher _managedDispatcher;
|
||||
private readonly CurlHttpDispatcher _curlDispatcher;
|
||||
private readonly IPlatformInfo _platformInfo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<bool> _curlTLSFallbackCache;
|
||||
|
||||
public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, IPlatformInfo platformInfo, Logger logger)
|
||||
{
|
||||
_managedDispatcher = managedDispatcher;
|
||||
_curlDispatcher = curlDispatcher;
|
||||
_platformInfo = platformInfo;
|
||||
_curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||
{
|
||||
if (PlatformInfo.IsMono && request.Url.Scheme == "https")
|
||||
{
|
||||
if (!_curlTLSFallbackCache.Find(request.Url.Host))
|
||||
{
|
||||
try
|
||||
{
|
||||
return _managedDispatcher.GetResponse(request, cookies);
|
||||
}
|
||||
catch (TlsFailureException)
|
||||
{
|
||||
_logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host);
|
||||
|
||||
_curlTLSFallbackCache.Set(request.Url.Host, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (_curlDispatcher.CheckAvailability())
|
||||
{
|
||||
return _curlDispatcher.GetResponse(request, cookies);
|
||||
}
|
||||
|
||||
_logger.Trace("Curl not available, using default WebClient.");
|
||||
}
|
||||
|
||||
return _managedDispatcher.GetResponse(request, cookies);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace Radarr.Host.AccessControl
|
||||
{
|
||||
public interface IRemoteAccessAdapter
|
||||
{
|
||||
void MakeAccessible(bool passive);
|
||||
}
|
||||
|
||||
public class RemoteAccessAdapter : IRemoteAccessAdapter
|
||||
{
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IUrlAclAdapter _urlAclAdapter;
|
||||
private readonly IFirewallAdapter _firewallAdapter;
|
||||
private readonly ISslAdapter _sslAdapter;
|
||||
|
||||
public RemoteAccessAdapter(IRuntimeInfo runtimeInfo,
|
||||
IUrlAclAdapter urlAclAdapter,
|
||||
IFirewallAdapter firewallAdapter,
|
||||
ISslAdapter sslAdapter)
|
||||
{
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_urlAclAdapter = urlAclAdapter;
|
||||
_firewallAdapter = firewallAdapter;
|
||||
_sslAdapter = sslAdapter;
|
||||
}
|
||||
|
||||
public void MakeAccessible(bool passive)
|
||||
{
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
if (_runtimeInfo.IsAdmin)
|
||||
{
|
||||
_firewallAdapter.MakeAccessible();
|
||||
_sslAdapter.Register();
|
||||
}
|
||||
else if (!passive)
|
||||
{
|
||||
throw new RemoteAccessException("Failed to register URLs for Radarr. Radarr will not be accessible remotely");
|
||||
}
|
||||
}
|
||||
|
||||
_urlAclAdapter.ConfigureUrls();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace Radarr.Host.AccessControl
|
||||
{
|
||||
public class RemoteAccessException : NzbDroneException
|
||||
{
|
||||
public RemoteAccessException(string message, params object[] args) : base(message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteAccessException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteAccessException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
|
||||
{
|
||||
}
|
||||
|
||||
public RemoteAccessException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Integration.Test.ApiTests.WantedTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CutoffUnmetFixture : IntegrationTest
|
||||
{
|
||||
[Test, Order(1)]
|
||||
public void cutoff_should_have_monitored_items()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", true);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
|
||||
|
||||
result.Records.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Test, Order(1)]
|
||||
public void cutoff_should_not_have_unmonitored_items()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", false);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
|
||||
|
||||
result.Records.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test, Order(1)]
|
||||
public void cutoff_should_not_have_released_items()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", true);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "inCinemas");
|
||||
|
||||
result.Records.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test, Order(1)]
|
||||
public void cutoff_should_have_movie()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", true);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc");
|
||||
|
||||
result.Records.First().Title.Should().Be("Pulp Fiction");
|
||||
}
|
||||
|
||||
[Test, Order(2)]
|
||||
public void cutoff_should_have_unmonitored_items()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", false);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "false");
|
||||
|
||||
result.Records.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Test, Order(2)]
|
||||
public void cutoff_should_have_released_items()
|
||||
{
|
||||
EnsureProfileCutoff(1, Quality.HDTV720p);
|
||||
var movie = EnsureMovie(680, "Pulp Fiction", false);
|
||||
EnsureMovieFile(movie, Quality.SDTV);
|
||||
|
||||
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "released");
|
||||
|
||||
result.Records.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue