mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3952ee402b | |||
| 0b3e27cb44 | |||
| 4fa4b3507e | |||
| 8c211364e2 | |||
| 2d9917d074 | |||
| d514699ab7 | |||
| dc176a83b3 | |||
| 69e3516a89 | |||
| c8a0f9fa7a | |||
| c2b9504b15 | |||
| 2693a3df2e | |||
| 8062466ab8 | |||
| 6cde1dd5ae | |||
| b6c4a97675 | |||
| a9444cef30 | |||
| bf217a7093 | |||
| b6b5355261 | |||
| bc37084ec4 | |||
| 0a1a30f2af | |||
| 7e023a7944 | |||
| 91f68de8a7 | |||
| 994e2a6c57 | |||
| 04da2d845a | |||
| d3b87bc3e8 |
+1
-1
@@ -16,7 +16,7 @@ Setup guides, FAQ, the more information we have on the wiki the better.
|
|||||||
### Getting started ###
|
### Getting started ###
|
||||||
|
|
||||||
1. Fork Sonarr
|
1. Fork Sonarr
|
||||||
2. Clone (develop branch)
|
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
|
||||||
3. Run `npm install`
|
3. Run `npm install`
|
||||||
4. Run `gulp watch` - Used to compile the UI components and copy them (leave this window open)
|
4. Run `gulp watch` - Used to compile the UI components and copy them (leave this window open)
|
||||||
5. Compile in Visual Studio
|
5. Compile in Visual Studio
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Function Build()
|
|||||||
|
|
||||||
Write-Host "Removing Mono.Posix.dll"
|
Write-Host "Removing Mono.Posix.dll"
|
||||||
Remove-Item "$outputFolder\Mono.Posix.dll"
|
Remove-Item "$outputFolder\Mono.Posix.dll"
|
||||||
|
Get-ChildItem $outputFolder -File -Filter "*.dylib" -Recurse | foreach ($_) {Remove-Item $_.Fullname}
|
||||||
|
|
||||||
Write-Host "##teamcity[progressFinish 'Build']"
|
Write-Host "##teamcity[progressFinish 'Build']"
|
||||||
}
|
}
|
||||||
@@ -233,6 +234,9 @@ Function RunGulp()
|
|||||||
Invoke-Expression 'gulp build' -ErrorAction Continue -Verbose
|
Invoke-Expression 'gulp build' -ErrorAction Continue -Verbose
|
||||||
CheckExitCode
|
CheckExitCode
|
||||||
|
|
||||||
|
Invoke-Expression 'gulp build --phantom' -ErrorAction Continue -Verbose
|
||||||
|
CheckExitCode
|
||||||
|
|
||||||
Write-Host "##teamcity[progressFinish 'Running Gulp']"
|
Write-Host "##teamcity[progressFinish 'Running Gulp']"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
|||||||
## Configuring Development Environment: ##
|
## Configuring Development Environment: ##
|
||||||
|
|
||||||
### Requirements ###
|
### Requirements ###
|
||||||
- Visual Studio 2013 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
|
- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
|
||||||
- [Git](http://git-scm.com/downloads)
|
- [Git](http://git-scm.com/downloads)
|
||||||
- [NodeJS](http://nodejs.org/download/)
|
- [NodeJS](http://nodejs.org/download/)
|
||||||
- [Gulp](http://gulpjs.com)
|
- [Gulp](http://gulpjs.com)
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ using NzbDrone.Core.MediaFiles.Events;
|
|||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.SeriesStats;
|
using NzbDrone.Core.SeriesStats;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Api.Validation;
|
|
||||||
using NzbDrone.Api.Mapping;
|
using NzbDrone.Api.Mapping;
|
||||||
using NzbDrone.Core.Tv.Events;
|
using NzbDrone.Core.Tv.Events;
|
||||||
using NzbDrone.Core.Validation.Paths;
|
using NzbDrone.Core.Validation.Paths;
|
||||||
using NzbDrone.Core.DataAugmentation.Scene;
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Series
|
namespace NzbDrone.Api.Series
|
||||||
@@ -43,7 +43,8 @@ namespace NzbDrone.Api.Series
|
|||||||
SeriesPathValidator seriesPathValidator,
|
SeriesPathValidator seriesPathValidator,
|
||||||
SeriesExistsValidator seriesExistsValidator,
|
SeriesExistsValidator seriesExistsValidator,
|
||||||
DroneFactoryValidator droneFactoryValidator,
|
DroneFactoryValidator droneFactoryValidator,
|
||||||
SeriesAncestorValidator seriesAncestorValidator
|
SeriesAncestorValidator seriesAncestorValidator,
|
||||||
|
ProfileExistsValidator profileExistsValidator
|
||||||
)
|
)
|
||||||
: base(signalRBroadcaster)
|
: base(signalRBroadcaster)
|
||||||
{
|
{
|
||||||
@@ -59,7 +60,7 @@ namespace NzbDrone.Api.Series
|
|||||||
UpdateResource = UpdateSeries;
|
UpdateResource = UpdateSeries;
|
||||||
DeleteResource = DeleteSeries;
|
DeleteResource = DeleteSeries;
|
||||||
|
|
||||||
SharedValidator.RuleFor(s => s.ProfileId).ValidId();
|
Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId));
|
||||||
|
|
||||||
SharedValidator.RuleFor(s => s.Path)
|
SharedValidator.RuleFor(s => s.Path)
|
||||||
.Cascade(CascadeMode.StopOnFirstFailure)
|
.Cascade(CascadeMode.StopOnFirstFailure)
|
||||||
@@ -70,6 +71,8 @@ namespace NzbDrone.Api.Series
|
|||||||
.SetValidator(seriesAncestorValidator)
|
.SetValidator(seriesAncestorValidator)
|
||||||
.When(s => !s.Path.IsNullOrWhiteSpace());
|
.When(s => !s.Path.IsNullOrWhiteSpace());
|
||||||
|
|
||||||
|
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
|
||||||
|
|
||||||
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
|
||||||
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
|
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
|
||||||
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
PostValidator.RuleFor(s => s.Title).NotEmpty();
|
||||||
|
|||||||
@@ -1,110 +1,59 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Configuration
|
namespace NzbDrone.Core.Test.Configuration
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ConfigServiceFixture : DbTest<ConfigService, Config>
|
public class ConfigServiceFixture : TestBase<ConfigService>
|
||||||
{
|
{
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
Mocker.SetConstant<IConfigRepository>(Mocker.Resolve<ConfigRepository>());
|
|
||||||
|
|
||||||
Db.All<Config>().ForEach(Db.Delete);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Add_new_value_to_database()
|
public void Add_new_value_to_database()
|
||||||
{
|
{
|
||||||
const string key = "MY_KEY";
|
const string key = "RssSyncInterval";
|
||||||
const string value = "MY_VALUE";
|
const int value = 12;
|
||||||
|
|
||||||
Subject.SetValue(key, value);
|
Subject.RssSyncInterval = value;
|
||||||
Subject.GetValue(key, "").Should().Be(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
AssertUpsert(key, value);
|
||||||
public void Get_value_from_database()
|
|
||||||
{
|
|
||||||
const string key = "MY_KEY";
|
|
||||||
const string value = "MY_VALUE";
|
|
||||||
|
|
||||||
|
|
||||||
Db.Insert(new Config { Key = key, Value = value });
|
|
||||||
Db.Insert(new Config { Key = "Other Key", Value = "OtherValue" });
|
|
||||||
|
|
||||||
var result = Subject.GetValue(key, "");
|
|
||||||
|
|
||||||
result.Should().Be(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Get_value_should_return_default_when_no_value()
|
public void Get_value_should_return_default_when_no_value()
|
||||||
{
|
{
|
||||||
const string key = "MY_KEY";
|
Subject.RssSyncInterval.Should().Be(15);
|
||||||
const string value = "MY_VALUE";
|
|
||||||
|
|
||||||
var result = Subject.GetValue(key, value);
|
|
||||||
|
|
||||||
result.Should().Be(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void New_value_should_update_old_value_new_value()
|
|
||||||
{
|
|
||||||
const string key = "MY_KEY";
|
|
||||||
const string originalValue = "OLD_VALUE";
|
|
||||||
const string newValue = "NEW_VALUE";
|
|
||||||
|
|
||||||
Db.Insert(new Config { Key = key, Value = originalValue });
|
|
||||||
|
|
||||||
Subject.SetValue(key, newValue);
|
|
||||||
var result = Subject.GetValue(key, "");
|
|
||||||
|
|
||||||
|
|
||||||
result.Should().Be(newValue);
|
|
||||||
AllStoredModels.Should().HaveCount(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void New_value_should_update_old_value_same_value()
|
|
||||||
{
|
|
||||||
const string key = "MY_KEY";
|
|
||||||
const string value = "OLD_VALUE";
|
|
||||||
|
|
||||||
Subject.SetValue(key, value);
|
|
||||||
Subject.SetValue(key, value);
|
|
||||||
var result = Subject.GetValue(key, "");
|
|
||||||
|
|
||||||
result.Should().Be(value);
|
|
||||||
AllStoredModels.Should().HaveCount(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void get_value_with_persist_should_store_default_value()
|
public void get_value_with_persist_should_store_default_value()
|
||||||
{
|
{
|
||||||
const string key = "MY_KEY";
|
var salt = Subject.HmacSalt;
|
||||||
string value = Guid.NewGuid().ToString();
|
salt.Should().NotBeNullOrWhiteSpace();
|
||||||
|
AssertUpsert("HmacSalt", salt);
|
||||||
Subject.GetValue(key, value, persist: true).Should().Be(value);
|
|
||||||
Subject.GetValue(key, string.Empty).Should().Be(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void get_value_with_out_persist_should_not_store_default_value()
|
public void get_value_with_out_persist_should_not_store_default_value()
|
||||||
{
|
{
|
||||||
const string key = "MY_KEY";
|
var interval = Subject.RssSyncInterval;
|
||||||
string value1 = Guid.NewGuid().ToString();
|
interval.Should().Be(15);
|
||||||
string value2 = Guid.NewGuid().ToString();
|
Mocker.GetMock<IConfigRepository>().Verify(c => c.Insert(It.IsAny<Config>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
Subject.GetValue(key, value1).Should().Be(value1);
|
private void AssertUpsert(string key, object value)
|
||||||
Subject.GetValue(key, value2).Should().Be(value2);
|
{
|
||||||
|
Mocker.GetMock<IConfigRepository>().Verify(c => c.Upsert(key.ToLowerInvariant(), value.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -114,7 +63,16 @@ namespace NzbDrone.Core.Test.Configuration
|
|||||||
var configProvider = Subject;
|
var configProvider = Subject;
|
||||||
var allProperties = typeof(ConfigService).GetProperties().Where(p => p.GetSetMethod() != null).ToList();
|
var allProperties = typeof(ConfigService).GetProperties().Where(p => p.GetSetMethod() != null).ToList();
|
||||||
|
|
||||||
|
var keys = new List<string>();
|
||||||
|
var values = new List<Config>();
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigRepository>().Setup(c => c.Upsert(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, value) =>
|
||||||
|
{
|
||||||
|
keys.Add(key);
|
||||||
|
values.Add(new Config { Key = key, Value = value });
|
||||||
|
});
|
||||||
|
|
||||||
|
Mocker.GetMock<IConfigRepository>().Setup(c => c.All()).Returns(values);
|
||||||
|
|
||||||
foreach (var propertyInfo in allProperties)
|
foreach (var propertyInfo in allProperties)
|
||||||
{
|
{
|
||||||
@@ -148,8 +106,7 @@ namespace NzbDrone.Core.Test.Configuration
|
|||||||
returnValue.Should().Be(value, propertyInfo.Name);
|
returnValue.Should().Be(value, propertyInfo.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
AllStoredModels.Should()
|
keys.Should().OnlyHaveUniqueItems();
|
||||||
.HaveSameCount(allProperties, "two different properties are writing to the same key in db. Copy/Past fail.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
|
|||||||
[TestCase("Hawaii Five-0", Result = "Hawaii+Five+0")]
|
[TestCase("Hawaii Five-0", Result = "Hawaii+Five+0")]
|
||||||
[TestCase("Franklin & Bash", Result = "Franklin+and+Bash")]
|
[TestCase("Franklin & Bash", Result = "Franklin+and+Bash")]
|
||||||
[TestCase("Chicago P.D.", Result = "Chicago+PD")]
|
[TestCase("Chicago P.D.", Result = "Chicago+PD")]
|
||||||
[TestCase("Kourtney And Khloé Take The Hamptons", Result = "Kourtney+And+Khloe+Take+The+Hamptons")]
|
[TestCase("Kourtney And Khlo\u00E9 Take The Hamptons", Result = "Kourtney+And+Khloe+Take+The+Hamptons")]
|
||||||
public string should_replace_some_special_characters(string input)
|
public string should_replace_some_special_characters(string input)
|
||||||
{
|
{
|
||||||
Subject.SceneTitles = new List<string> { input };
|
Subject.SceneTitles = new List<string> { input };
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
_videoFiles = videoFiles.ToList();
|
_videoFiles = videoFiles.ToList();
|
||||||
|
|
||||||
Mocker.GetMock<IMediaFileService>()
|
Mocker.GetMock<IMediaFileService>()
|
||||||
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
|
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<Series>()))
|
||||||
.Returns(_videoFiles);
|
.Returns(_videoFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -180,21 +180,27 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_file_quality_if_folder_quality_is_lower_than_file_quality()
|
public void should_use_file_quality_if_file_quality_was_determined_by_name()
|
||||||
{
|
{
|
||||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||||
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
|
var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single());
|
||||||
|
|
||||||
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.SDTV)}, true);
|
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo{Quality = new QualityModel(Quality.Bluray1080p)}, true);
|
||||||
|
|
||||||
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
|
result.Single().LocalEpisode.Quality.Should().Be(expectedQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_folder_quality_when_it_is_greater_than_file_quality()
|
public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension()
|
||||||
{
|
{
|
||||||
GivenSpecifications(_pass1, _pass2, _pass3);
|
GivenSpecifications(_pass1, _pass2, _pass3);
|
||||||
var expectedQuality = new QualityModel(Quality.Bluray1080p);
|
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
|
||||||
|
|
||||||
|
_localEpisode.Path = _videoFiles.Single();
|
||||||
|
_localEpisode.Quality.QualitySource = QualitySource.Extension;
|
||||||
|
_localEpisode.Quality.Quality = Quality.HDTV720p;
|
||||||
|
|
||||||
|
var expectedQuality = new QualityModel(Quality.SDTV);
|
||||||
|
|
||||||
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
|
var result = Subject.GetImportDecisions(_videoFiles, _series, new ParsedEpisodeInfo { Quality = expectedQuality }, true);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
_episodeFile.Quality.Revision.Version = 2;
|
_episodeFile.Quality.Revision.Version = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GivenReal()
|
||||||
|
{
|
||||||
|
_episodeFile.Quality.Revision.Real = 1;
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_replace_Series_space_Title()
|
public void should_replace_Series_space_Title()
|
||||||
{
|
{
|
||||||
@@ -207,6 +212,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
.Should().Be("Proper");
|
.Should().Be("Proper");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_replace_quality_real_with_real()
|
||||||
|
{
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Quality Real}";
|
||||||
|
GivenReal();
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||||
|
.Should().Be("REAL");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_replace_all_contents_in_pattern()
|
public void should_replace_all_contents_in_pattern()
|
||||||
{
|
{
|
||||||
@@ -617,6 +632,16 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
|||||||
.Should().Be("South Park - S15E06 [HDTV-720p Proper]");
|
.Should().Be("South Park - S15E06 [HDTV-720p Proper]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_replace_quality_full_with_quality_title_and_real_when_a_real()
|
||||||
|
{
|
||||||
|
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} [{Quality Full}]";
|
||||||
|
GivenReal();
|
||||||
|
|
||||||
|
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
|
||||||
|
.Should().Be("South Park - S15E06 [HDTV-720p REAL]");
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(' ')]
|
[TestCase(' ')]
|
||||||
[TestCase('-')]
|
[TestCase('-')]
|
||||||
[TestCase('.')]
|
[TestCase('.')]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using FluentAssertions;
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
@@ -14,18 +13,19 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Chuck.S03E17.REAL.PROPER.720p.HDTV.x264-ORENJI-RP", 1)]
|
[TestCase("Chuck.S03E17.REAL.PROPER.720p.HDTV.x264-ORENJI-RP", 1)]
|
||||||
[TestCase("Covert.Affairs.S05E09.REAL.PROPER.HDTV.x264-KILLERS", 1)]
|
[TestCase("Covert.Affairs.S05E09.REAL.PROPER.HDTV.x264-KILLERS", 1)]
|
||||||
[TestCase("Mythbusters.S14E01.REAL.PROPER.720p.HDTV.x264-KILLERS", 1)]
|
[TestCase("Mythbusters.S14E01.REAL.PROPER.720p.HDTV.x264-KILLERS", 1)]
|
||||||
[TestCase("Orange.Is.the.New.Black.s02e06.real.proper.720p.webrip.x264-2hd", 1)]
|
[TestCase("Orange.Is.the.New.Black.s02e06.real.proper.720p.webrip.x264-2hd", 0)]
|
||||||
[TestCase("Top.Gear.S21E07.Super.Duper.Real.Proper.HDTV.x264-FTP", 1)]
|
[TestCase("Top.Gear.S21E07.Super.Duper.Real.Proper.HDTV.x264-FTP", 0)]
|
||||||
[TestCase("Top.Gear.S21E07.PROPER.HDTV.x264-RiVER-RP", 0)]
|
[TestCase("Top.Gear.S21E07.PROPER.HDTV.x264-RiVER-RP", 0)]
|
||||||
[TestCase("House.S07E11.PROPER.REAL.RERIP.1080p.BluRay.x264-TENEIGHTY", 1)]
|
[TestCase("House.S07E11.PROPER.REAL.RERIP.1080p.BluRay.x264-TENEIGHTY", 1)]
|
||||||
[TestCase("[MGS] - Kuragehime - Episode 02v2 - [D8B6C90D]", 0)]
|
[TestCase("[MGS] - Kuragehime - Episode 02v2 - [D8B6C90D]", 0)]
|
||||||
[TestCase("[Hatsuyuki] Tokyo Ghoul - 07 [v2][848x480][23D8F455].avi", 0)]
|
[TestCase("[Hatsuyuki] Tokyo Ghoul - 07 [v2][848x480][23D8F455].avi", 0)]
|
||||||
[TestCase("[DeadFish] Barakamon - 01v3 [720p][AAC]", 0)]
|
[TestCase("[DeadFish] Barakamon - 01v3 [720p][AAC]", 0)]
|
||||||
[TestCase("[DeadFish] Momo Kyun Sword - 01v4 [720p][AAC]", 0)]
|
[TestCase("[DeadFish] Momo Kyun Sword - 01v4 [720p][AAC]", 0)]
|
||||||
|
[TestCase("The Real Housewives of Some Place - S01E01 - Why are we doing this?", 0)]
|
||||||
public void should_parse_reality_from_title(string title, int reality)
|
public void should_parse_reality_from_title(string title, int reality)
|
||||||
{
|
{
|
||||||
//TODO: re-enable this when we have a reliable way to determine real
|
//TODO: re-enable this when we have a reliable way to determine real
|
||||||
//QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality);
|
QualityParser.ParseQuality(title).Revision.Real.Should().Be(reality);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("Chuck.S04E05.HDTV.XviD-LOL", 1)]
|
[TestCase("Chuck.S04E05.HDTV.XviD-LOL", 1)]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("The Young And The Restless - S42 Ep10718 - Ep10722", "The Young And The Restless", 42, new[] { 10718, 10719, 10720, 10721, 10722 })]
|
[TestCase("The Young And The Restless - S42 Ep10718 - Ep10722", "The Young And The Restless", 42, new[] { 10718, 10719, 10720, 10721, 10722 })]
|
||||||
[TestCase("The Young And The Restless - S42 Ep10688 - Ep10692", "The Young And The Restless", 42, new[] { 10688, 10689, 10690, 10691, 10692 })]
|
[TestCase("The Young And The Restless - S42 Ep10688 - Ep10692", "The Young And The Restless", 42, new[] { 10688, 10689, 10690, 10691, 10692 })]
|
||||||
[TestCase("RWBY.S01E02E03.1080p.BluRay.x264-DeBTViD", "RWBY", 1, new [] { 2, 3 })]
|
[TestCase("RWBY.S01E02E03.1080p.BluRay.x264-DeBTViD", "RWBY", 1, new [] { 2, 3 })]
|
||||||
|
[TestCase("grp-zoos01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })]
|
||||||
|
[TestCase("grp-zoo-s01e11e12-1080p", "grp-zoo", 1, new [] { 11, 12 })]
|
||||||
//[TestCase("", "", , new [] { })]
|
//[TestCase("", "", , new [] { })]
|
||||||
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_remove_accents_from_title()
|
public void should_remove_accents_from_title()
|
||||||
{
|
{
|
||||||
const string title = "Carnivŕle";
|
const string title = "Carniv\u00E0le";
|
||||||
|
|
||||||
title.CleanSeriesTitle().Should().Be("carnivale");
|
title.CleanSeriesTitle().Should().Be("carnivale");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,24 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("Saturday.Night.Live.Vintage.S10E09.Eddie.Murphy.The.Honeydrippers.1080i.UPSCALE.HDTV.DD5.1.MPEG2-zebra")]
|
||||||
|
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]")]
|
||||||
|
[TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]")]
|
||||||
|
[TestCase("White.Van.Man.2011.S02E01.WS.PDTV.x264-REPACK-TLA")]
|
||||||
|
public void should_parse_quality_from_name(string title)
|
||||||
|
{
|
||||||
|
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("Revolution.S01E02.Chained.Heat.mkv")]
|
||||||
|
[TestCase("Dexter - S01E01 - Title.avi")]
|
||||||
|
[TestCase("the_x-files.9x18.sunshine_days.avi")]
|
||||||
|
[TestCase("[CR] Sailor Moon - 004 [48CE2D0F].avi")]
|
||||||
|
public void should_parse_quality_from_extension(string title)
|
||||||
|
{
|
||||||
|
QualityParser.ParseQuality(title).QualitySource.Should().Be(QualitySource.Extension);
|
||||||
|
}
|
||||||
|
|
||||||
private void ParseAndVerifyQuality(string title, Quality quality, bool proper)
|
private void ParseAndVerifyQuality(string title, Quality quality, bool proper)
|
||||||
{
|
{
|
||||||
var result = QualityParser.ParseQuality(title);
|
var result = QualityParser.ParseQuality(title);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Series Title S01E01 Episode Title", null)]
|
[TestCase("Series Title S01E01 Episode Title", null)]
|
||||||
[TestCase("The Colbert Report - 2014-06-02 - Thomas Piketty.mkv", null)]
|
[TestCase("The Colbert Report - 2014-06-02 - Thomas Piketty.mkv", null)]
|
||||||
[TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", null)]
|
[TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", null)]
|
||||||
[TestCase("Reizen Waes - S01E08 - Transistrië, Zuid-Ossetië en Abchazië SDTV.avi", null)]
|
[TestCase("Reizen Waes - S01E08 - Transistri\u00EB, Zuid-Osseti\u00EB en Abchazi\u00EB SDTV.avi", null)]
|
||||||
[TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", null)]
|
[TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", null)]
|
||||||
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "DIMENSION")]
|
[TestCase("[ www.Torrenting.com ] - Revenge.S03E14.720p.HDTV.X264-DIMENSION", "DIMENSION")]
|
||||||
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "2HD")]
|
[TestCase("Seed S02E09 HDTV x264-2HD [eztv]-[rarbg.com]", "2HD")]
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("The Young And the Restless - S42 E10713 - 2015-07-20.mp4", "The Young And the Restless", 42, 10713)]
|
[TestCase("The Young And the Restless - S42 E10713 - 2015-07-20.mp4", "The Young And the Restless", 42, 10713)]
|
||||||
[TestCase("quantico.103.hdtv-lol[ettv].mp4", "quantico", 1, 3)]
|
[TestCase("quantico.103.hdtv-lol[ettv].mp4", "quantico", 1, 3)]
|
||||||
[TestCase("Fargo - 01x02 - The Rooster Prince - [itz_theo]", "Fargo", 1, 2)]
|
[TestCase("Fargo - 01x02 - The Rooster Prince - [itz_theo]", "Fargo", 1, 2)]
|
||||||
|
[TestCase("Castle (2009) - [06x16] - Room 147.mp4", "Castle (2009)", 6, 16)]
|
||||||
|
[TestCase("grp-zoos01e11-1080p", "grp-zoo", 1, 11)]
|
||||||
|
[TestCase("grp-zoo-s01e11-1080p", "grp-zoo", 1, 11)]
|
||||||
//[TestCase("", "", 0, 0)]
|
//[TestCase("", "", 0, 0)]
|
||||||
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
{
|
{
|
||||||
_series = Builder<Series>.CreateNew()
|
_series = Builder<Series>.CreateNew()
|
||||||
.With(v => v.Status == SeriesStatusType.Continuing)
|
.With(v => v.Status == SeriesStatusType.Continuing)
|
||||||
.With(v => v.LastInfoSync == DateTime.UtcNow.AddHours(-12))
|
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>()
|
Mocker.GetMock<IEpisodeService>()
|
||||||
@@ -45,6 +44,11 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
_series.LastInfoSync = DateTime.UtcNow.AddDays(-1);
|
_series.LastInfoSync = DateTime.UtcNow.AddDays(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GivenSeriesLastRefreshedHalfADayAgo()
|
||||||
|
{
|
||||||
|
_series.LastInfoSync = DateTime.UtcNow.AddHours(-12);
|
||||||
|
}
|
||||||
|
|
||||||
private void GivenSeriesLastRefreshedRecently()
|
private void GivenSeriesLastRefreshedRecently()
|
||||||
{
|
{
|
||||||
_series.LastInfoSync = DateTime.UtcNow.AddHours(-1);
|
_series.LastInfoSync = DateTime.UtcNow.AddHours(-1);
|
||||||
@@ -66,6 +70,8 @@ namespace NzbDrone.Core.Test.TvTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_running_series_last_refreshed_more_than_6_hours_ago()
|
public void should_return_true_if_running_series_last_refreshed_more_than_6_hours_ago()
|
||||||
{
|
{
|
||||||
|
GivenSeriesLastRefreshedHalfADayAgo();
|
||||||
|
|
||||||
Subject.ShouldRefresh(_series).Should().BeTrue();
|
Subject.ShouldRefresh(_series).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public interface IConfigRepository : IBasicRepository<Config>
|
public interface IConfigRepository : IBasicRepository<Config>
|
||||||
{
|
{
|
||||||
Config Get(string key);
|
Config Get(string key);
|
||||||
|
Config Upsert(string key, string value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConfigRepository : BasicRepository<Config>, IConfigRepository
|
public class ConfigRepository : BasicRepository<Config>, IConfigRepository
|
||||||
@@ -23,5 +23,19 @@ namespace NzbDrone.Core.Configuration
|
|||||||
{
|
{
|
||||||
return Query.Where(c => c.Key == key).SingleOrDefault();
|
return Query.Where(c => c.Key == key).SingleOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Config Upsert(string key, string value)
|
||||||
|
{
|
||||||
|
var dbValue = Get(key);
|
||||||
|
|
||||||
|
if (dbValue == null)
|
||||||
|
{
|
||||||
|
return Insert(new Config {Key = key, Value = value});
|
||||||
|
}
|
||||||
|
|
||||||
|
dbValue.Value = value;
|
||||||
|
|
||||||
|
return Update(dbValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
_cache = new Dictionary<string, string>();
|
_cache = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Config> All()
|
private Dictionary<string, object> AllWithDefaults()
|
||||||
{
|
|
||||||
return _repository.All();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, object> AllWithDefaults()
|
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
|
var dict = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
@@ -45,7 +40,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
foreach (var propertyInfo in properties)
|
foreach (var propertyInfo in properties)
|
||||||
{
|
{
|
||||||
var value = propertyInfo.GetValue(this, null);
|
var value = propertyInfo.GetValue(this, null);
|
||||||
|
|
||||||
dict.Add(propertyInfo.Name, value);
|
dict.Add(propertyInfo.Name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +59,9 @@ namespace NzbDrone.Core.Configuration
|
|||||||
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
|
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
|
||||||
|
|
||||||
if (!equal)
|
if (!equal)
|
||||||
|
{
|
||||||
SetValue(configValue.Key, configValue.Value.ToString());
|
SetValue(configValue.Key, configValue.Value.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_eventAggregator.PublishEvent(new ConfigSavedEvent());
|
_eventAggregator.PublishEvent(new ConfigSavedEvent());
|
||||||
@@ -331,7 +327,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
return Convert.ToInt32(GetValue(key, defaultValue));
|
return Convert.ToInt32(GetValue(key, defaultValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetValueEnum<T>(string key, T defaultValue)
|
private T GetValueEnum<T>(string key, T defaultValue)
|
||||||
{
|
{
|
||||||
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), true);
|
||||||
}
|
}
|
||||||
@@ -346,7 +342,9 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string dbValue;
|
string dbValue;
|
||||||
|
|
||||||
if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue))
|
if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !string.IsNullOrEmpty(dbValue))
|
||||||
|
{
|
||||||
return dbValue;
|
return dbValue;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.Trace("Using default config value for '{0}' defaultValue:'{1}'", key, defaultValue);
|
_logger.Trace("Using default config value for '{0}' defaultValue:'{1}'", key, defaultValue);
|
||||||
|
|
||||||
@@ -354,6 +352,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
{
|
{
|
||||||
SetValue(key, defaultValue.ToString());
|
SetValue(key, defaultValue.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue.ToString();
|
return defaultValue.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,44 +366,34 @@ namespace NzbDrone.Core.Configuration
|
|||||||
SetValue(key, value.ToString());
|
SetValue(key, value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue(string key, string value)
|
private void SetValue(string key, Enum value)
|
||||||
|
{
|
||||||
|
SetValue(key, value.ToString().ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetValue(string key, string value)
|
||||||
{
|
{
|
||||||
key = key.ToLowerInvariant();
|
key = key.ToLowerInvariant();
|
||||||
|
|
||||||
_logger.Trace("Writing Setting to database. Key:'{0}' Value:'{1}'", key, value);
|
_logger.Trace("Writing Setting to database. Key:'{0}' Value:'{1}'", key, value);
|
||||||
|
_repository.Upsert(key, value);
|
||||||
var dbValue = _repository.Get(key);
|
|
||||||
|
|
||||||
if (dbValue == null)
|
|
||||||
{
|
|
||||||
_repository.Insert(new Config { Key = key, Value = value });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dbValue.Value = value;
|
|
||||||
_repository.Update(dbValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearCache();
|
ClearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue(string key, Enum value)
|
|
||||||
{
|
|
||||||
SetValue(key, value.ToString().ToLower());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureCache()
|
private void EnsureCache()
|
||||||
{
|
{
|
||||||
lock (_cache)
|
lock (_cache)
|
||||||
{
|
{
|
||||||
if (!_cache.Any())
|
if (!_cache.Any())
|
||||||
{
|
{
|
||||||
_cache = All().ToDictionary(c => c.Key.ToLower(), c => c.Value);
|
var all = _repository.All();
|
||||||
|
_cache = all.ToDictionary(c => c.Key.ToLower(), c => c.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ClearCache()
|
private static void ClearCache()
|
||||||
{
|
{
|
||||||
lock (_cache)
|
lock (_cache)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ namespace NzbDrone.Core.Configuration
|
|||||||
{
|
{
|
||||||
public interface IConfigService
|
public interface IConfigService
|
||||||
{
|
{
|
||||||
IEnumerable<Config> All();
|
|
||||||
Dictionary<string, object> AllWithDefaults();
|
|
||||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||||
|
|
||||||
bool IsDefined(string key);
|
bool IsDefined(string key);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System.Text.RegularExpressions;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
|||||||
|
|
||||||
RuleFor(c => c.UrlBase).ValidUrlBase();
|
RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||||
|
|
||||||
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$").WithMessage("Allowed characters a-z and -");
|
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Indexers
|
|||||||
IndexerStatus blockedIndexerStatus;
|
IndexerStatus blockedIndexerStatus;
|
||||||
if (blockedIndexers.TryGetValue(indexer.Definition.Id, out blockedIndexerStatus))
|
if (blockedIndexers.TryGetValue(indexer.Definition.Id, out blockedIndexerStatus))
|
||||||
{
|
{
|
||||||
_logger.Debug("Temporarily ignoring indexer {0} till {1} due to recent failures.", indexer.Definition.Name, blockedIndexerStatus.DisabledTill.Value);
|
_logger.Debug("Temporarily ignoring indexer {0} till {1} due to recent failures.", indexer.Definition.Name, blockedIndexerStatus.DisabledTill.Value.ToLocalTime());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
yield return GetDefinition("nzbplanet.net", GetSettings("https://nzbplanet.net"));
|
yield return GetDefinition("nzbplanet.net", GetSettings("https://nzbplanet.net"));
|
||||||
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
|
||||||
yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com"));
|
yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com"));
|
||||||
|
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||||||
|
|
||||||
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
|
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
|
||||||
{
|
{
|
||||||
if (folderInfo != null &&
|
if (folderInfo != null && folderInfo.Quality.Quality != Quality.Unknown && fileQuality.QualitySource == QualitySource.Extension)
|
||||||
folderInfo.Quality.Quality != Quality.Unknown &&
|
|
||||||
new QualityModelComparer(series.Profile).Compare(folderInfo.Quality, fileQuality) > 0)
|
|
||||||
{
|
{
|
||||||
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
|
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
|
||||||
return folderInfo.Quality;
|
return folderInfo.Quality;
|
||||||
|
|||||||
@@ -767,6 +767,7 @@
|
|||||||
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />
|
||||||
<Compile Include="Profiles\ProfileRepository.cs" />
|
<Compile Include="Profiles\ProfileRepository.cs" />
|
||||||
<Compile Include="ProgressMessaging\ProgressMessageContext.cs" />
|
<Compile Include="ProgressMessaging\ProgressMessageContext.cs" />
|
||||||
|
<Compile Include="Qualities\QualitySource.cs" />
|
||||||
<Compile Include="Qualities\Revision.cs" />
|
<Compile Include="Qualities\Revision.cs" />
|
||||||
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
||||||
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
||||||
@@ -1003,6 +1004,7 @@
|
|||||||
<Compile Include="Validation\Paths\SeriesAncestorValidator.cs" />
|
<Compile Include="Validation\Paths\SeriesAncestorValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\SeriesExistsValidator.cs" />
|
<Compile Include="Validation\Paths\SeriesExistsValidator.cs" />
|
||||||
<Compile Include="Validation\Paths\SeriesPathValidator.cs" />
|
<Compile Include="Validation\Paths\SeriesPathValidator.cs" />
|
||||||
|
<Compile Include="Validation\ProfileExistsValidator.cs" />
|
||||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||||
<Compile Include="Validation\UrlValidator.cs" />
|
<Compile Include="Validation\UrlValidator.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -1063,6 +1065,14 @@
|
|||||||
<Link>MediaInfo.dll</Link>
|
<Link>MediaInfo.dll</Link>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="..\Libraries\MediaInfo\libmediainfo.0.dylib">
|
||||||
|
<Link>libmediainfo.0.dylib</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="..\Libraries\Sqlite\libsqlite3.0.dylib">
|
||||||
|
<Link>libsqlite3.0.dylib</Link>
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup />
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
|||||||
@@ -432,10 +432,12 @@ namespace NzbDrone.Core.Organizer
|
|||||||
{
|
{
|
||||||
var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title;
|
var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title;
|
||||||
var qualityProper = GetQualityProper(series, episodeFile.Quality);
|
var qualityProper = GetQualityProper(series, episodeFile.Quality);
|
||||||
|
var qualityReal = GetQualityReal(series, episodeFile.Quality);
|
||||||
|
|
||||||
tokenHandlers["{Quality Full}"] = m => string.Format("{0} {1}", qualityTitle, qualityProper);
|
tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal);
|
||||||
tokenHandlers["{Quality Title}"] = m => qualityTitle;
|
tokenHandlers["{Quality Title}"] = m => qualityTitle;
|
||||||
tokenHandlers["{Quality Proper}"] = m => qualityProper;
|
tokenHandlers["{Quality Proper}"] = m => qualityProper;
|
||||||
|
tokenHandlers["{Quality Real}"] = m => qualityReal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile)
|
private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile)
|
||||||
@@ -708,6 +710,16 @@ namespace NzbDrone.Core.Organizer
|
|||||||
return "Proper";
|
return "Proper";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetQualityReal(Series series, QualityModel quality)
|
||||||
|
{
|
||||||
|
if (quality.Revision.Real > 0)
|
||||||
|
{
|
||||||
|
return "REAL";
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ namespace NzbDrone.Core.Parser
|
|||||||
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:[-_\W](?<![()\[]))+(?:\W?Season\W?)(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)+(?:Episode\W)(?:[-_. ]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
|
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:[-_\W](?<![()\[]))+(?:\W?Season\W?)(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)+(?:Episode\W)(?:[-_. ]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
|
//Multi-episode release with no space between series title and season (S01E11E12)
|
||||||
|
new Regex(@"(?:.*(?:^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{2}(?!\d+))(?:E(?<episode>(?<!\d+)\d{2}(?!\d+)))+",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Single episode season or episode S1E1 or S1-E1
|
//Single episode season or episode S1E1 or S1-E1
|
||||||
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E(?<episode>(?<!\d+)\d{1,2}(?!\d+))",
|
new Regex(@"(?:.*(?:\""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E(?<episode>(?<!\d+)\d{1,2}(?!\d+))",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
@@ -106,6 +110,10 @@ namespace NzbDrone.Core.Parser
|
|||||||
new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{4}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)",
|
new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{4}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
|
//Episodes with a title and season/episode in square brackets
|
||||||
|
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+\[S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+|i|p)))+\])\W?(?!\\)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Supports 103/113 naming
|
//Supports 103/113 naming
|
||||||
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+",
|
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
@@ -128,10 +136,6 @@ namespace NzbDrone.Core.Parser
|
|||||||
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)",
|
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|
||||||
//Episodes with a title and season/episode in square brackets
|
|
||||||
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+\[S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+|i|p)))+\])\W?(?!\\)",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
|
||||||
|
|
||||||
//Episodes with single digit episode number (S01E1, S01E5E6, etc)
|
//Episodes with single digit episode number (S01E1, S01E5E6, etc)
|
||||||
new Regex(@"^(?<title>.*?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)",
|
new Regex(@"^(?<title>.*?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ namespace NzbDrone.Core.Parser
|
|||||||
private static readonly Regex VersionRegex = new Regex(@"\dv(?<version>\d)\b|\[v(?<version>\d)\]",
|
private static readonly Regex VersionRegex = new Regex(@"\dv(?<version>\d)\b|\[v(?<version>\d)\]",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private static readonly Regex RealRegex = new Regex(@"\b(?<real>)real\b",
|
private static readonly Regex RealRegex = new Regex(@"\b(?<real>REAL)\b",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p|640x480|848x480)|(?<_576p>576p)|(?<_720p>720p|1280x720)|(?<_1080p>1080p|1920x1080))\b",
|
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p|640x480|848x480)|(?<_576p>576p)|(?<_720p>720p|1280x720)|(?<_1080p>1080p|1920x1080))\b",
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
@@ -56,8 +56,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
|
|
||||||
name = name.Trim();
|
name = name.Trim();
|
||||||
var normalizedName = name.Replace('_', ' ').Trim().ToLower();
|
var normalizedName = name.Replace('_', ' ').Trim().ToLower();
|
||||||
var result = ParseQualityModifiers(normalizedName);
|
var result = ParseQualityModifiers(name, normalizedName);
|
||||||
|
|
||||||
|
|
||||||
if (RawHDRegex.IsMatch(normalizedName))
|
if (RawHDRegex.IsMatch(normalizedName))
|
||||||
{
|
{
|
||||||
@@ -276,6 +275,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.Quality = MediaFileExtensions.GetQualityForExtension(Path.GetExtension(name));
|
result.Quality = MediaFileExtensions.GetQualityForExtension(Path.GetExtension(name));
|
||||||
|
result.QualitySource = QualitySource.Extension;
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
@@ -311,7 +311,7 @@ namespace NzbDrone.Core.Parser
|
|||||||
return Quality.Unknown;
|
return Quality.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static QualityModel ParseQualityModifiers(string normalizedName)
|
private static QualityModel ParseQualityModifiers(string name, string normalizedName)
|
||||||
{
|
{
|
||||||
var result = new QualityModel { Quality = Quality.Unknown };
|
var result = new QualityModel { Quality = Quality.Unknown };
|
||||||
|
|
||||||
@@ -329,12 +329,12 @@ namespace NzbDrone.Core.Parser
|
|||||||
|
|
||||||
//TODO: re-enable this when we have a reliable way to determine real
|
//TODO: re-enable this when we have a reliable way to determine real
|
||||||
//TODO: Only treat it as a real if it comes AFTER the season/epsiode number
|
//TODO: Only treat it as a real if it comes AFTER the season/epsiode number
|
||||||
// var realRegexResult = RealRegex.Matches(normalizedName);
|
var realRegexResult = RealRegex.Matches(name);
|
||||||
//
|
|
||||||
// if (realRegexResult.Count > 0)
|
if (realRegexResult.Count > 0)
|
||||||
// {
|
{
|
||||||
// result.Revision.Real = realRegexResult.Count;
|
result.Revision.Real = realRegexResult.Count;
|
||||||
// }
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace NzbDrone.Core.Profiles
|
|||||||
{
|
{
|
||||||
public interface IProfileRepository : IBasicRepository<Profile>
|
public interface IProfileRepository : IBasicRepository<Profile>
|
||||||
{
|
{
|
||||||
|
bool Exists(int id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProfileRepository : BasicRepository<Profile>, IProfileRepository
|
public class ProfileRepository : BasicRepository<Profile>, IProfileRepository
|
||||||
@@ -14,5 +14,10 @@ namespace NzbDrone.Core.Profiles
|
|||||||
: base(database, eventAggregator)
|
: base(database, eventAggregator)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Exists(int id)
|
||||||
|
{
|
||||||
|
return DataMapper.Query<Profile>().Where(p => p.Id == id).GetRowCount() == 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace NzbDrone.Core.Profiles
|
|||||||
void Delete(int id);
|
void Delete(int id);
|
||||||
List<Profile> All();
|
List<Profile> All();
|
||||||
Profile Get(int id);
|
Profile Get(int id);
|
||||||
|
bool Exists(int id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
||||||
@@ -61,6 +62,11 @@ namespace NzbDrone.Core.Profiles
|
|||||||
return _profileRepository.Get(id);
|
return _profileRepository.Get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Exists(int id)
|
||||||
|
{
|
||||||
|
return _profileRepository.Exists(id);
|
||||||
|
}
|
||||||
|
|
||||||
private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed)
|
private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed)
|
||||||
{
|
{
|
||||||
var items = Quality.DefaultQualityDefinitions
|
var items = Quality.DefaultQualityDefinitions
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Qualities
|
namespace NzbDrone.Core.Qualities
|
||||||
@@ -7,6 +8,9 @@ namespace NzbDrone.Core.Qualities
|
|||||||
{
|
{
|
||||||
public Quality Quality { get; set; }
|
public Quality Quality { get; set; }
|
||||||
public Revision Revision { get; set; }
|
public Revision Revision { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public QualitySource QualitySource { get; set; }
|
||||||
|
|
||||||
public QualityModel()
|
public QualityModel()
|
||||||
: this(Quality.Unknown, new Revision())
|
: this(Quality.Unknown, new Revision())
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.Qualities
|
||||||
|
{
|
||||||
|
public enum QualitySource
|
||||||
|
{
|
||||||
|
Name,
|
||||||
|
Extension,
|
||||||
|
MediaInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Core.Profiles;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Validation
|
||||||
|
{
|
||||||
|
public class ProfileExistsValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
private readonly IProfileService _profileService;
|
||||||
|
|
||||||
|
public ProfileExistsValidator(IProfileService profileService)
|
||||||
|
: base("Profile does not exist")
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return true;
|
||||||
|
|
||||||
|
return _profileService.Exists((int)context.PropertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,18 +32,18 @@ namespace NzbDrone.Update.Test
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_call_update_with_corret_path()
|
public void should_call_update_with_correct_path()
|
||||||
{
|
{
|
||||||
const string ProcessPath = @"C:\NzbDrone\nzbdrone.exe";
|
var ProcessPath = @"C:\NzbDrone\nzbdrone.exe".AsOsAgnostic();
|
||||||
|
|
||||||
Mocker.GetMock<IProcessProvider>().Setup(c => c.GetProcessById(12))
|
Mocker.GetMock<IProcessProvider>().Setup(c => c.GetProcessById(12))
|
||||||
.Returns(new ProcessInfo() { StartPath = ProcessPath });
|
.Returns(new ProcessInfo() { StartPath = ProcessPath });
|
||||||
|
|
||||||
|
|
||||||
Subject.Start(new[] { "12", "" });
|
Subject.Start(new[] { "12", "", ProcessPath });
|
||||||
|
|
||||||
|
|
||||||
Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\NzbDrone", 12), Times.Once());
|
Mocker.GetMock<IInstallUpdateService>().Verify(c => c.Start(@"C:\NzbDrone".AsOsAgnostic(), 12), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var $ = require('jquery');
|
var $ = require('jquery');
|
||||||
|
var _ = require('underscore');
|
||||||
var vent = require('../../vent');
|
var vent = require('../../vent');
|
||||||
var TemplatedCell = require('../../Cells/TemplatedCell');
|
var TemplatedCell = require('../../Cells/TemplatedCell');
|
||||||
var RemoveFromQueueView = require('./RemoveFromQueueView');
|
var RemoveFromQueueView = require('./RemoveFromQueueView');
|
||||||
@@ -40,11 +41,12 @@ module.exports = TemplatedCell.extend({
|
|||||||
|
|
||||||
_grab : function() {
|
_grab : function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var data = _.omit(this.model.toJSON(), 'series', 'episode');
|
||||||
|
|
||||||
var promise = $.ajax({
|
var promise = $.ajax({
|
||||||
url : window.NzbDrone.ApiRoot + '/queue/grab',
|
url : window.NzbDrone.ApiRoot + '/queue/grab',
|
||||||
type : 'POST',
|
type : 'POST',
|
||||||
data : JSON.stringify(this.model.toJSON())
|
data : JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$(this.ui.grab).spinForPromise(promise);
|
this.$(this.ui.grab).spinForPromise(promise);
|
||||||
|
|||||||
@@ -212,7 +212,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legend-labels {
|
.legend-labels {
|
||||||
width : 500px;
|
max-width : 100%;
|
||||||
|
width : 500px;
|
||||||
|
|
||||||
@media (max-width: @screen-xs-min) {
|
@media (max-width: @screen-xs-min) {
|
||||||
width : 400px;
|
width : 400px;
|
||||||
|
|||||||
Reference in New Issue
Block a user