New: Anime support
New: pull alternate names from thexem.de
New: Search using all alternate names (if rage ID is unavailable)
New: Show scene mapping information when hovering over episode number
New: Full season searching for anime (searches for each episode)
New: animezb.com anime indexer
New: Treat BD as bluray

Fixed: Parsing of 2 digit absolute episode numbers
Fixed: Loading series details page for series that start with period
Fixed: Return 0 results when manual search fails, instead of an error
Fixed: animezb URL
pull/3113/head
Mark McDowall 11 years ago
parent 828dd5f5ad
commit 193672b652

@ -51,7 +51,7 @@ namespace NzbDrone.Api.Test.MappingTests
}
[Test]
public void should_map_lay_loaded_values_should_not_be_inject_if_not_loaded()
public void should_map_lazy_loaded_values_should_not_be_inject_if_not_loaded()
{
var modelWithLazy = new ModelWithLazy()
{

@ -1,16 +1,18 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace NzbDrone.Api.ClientSchema
{
public class Field
{
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public Int32 Order { get; set; }
public String Name { get; set; }
public String Label { get; set; }
public String HelpText { get; set; }
public String HelpLink { get; set; }
public Object Value { get; set; }
public String Type { get; set; }
public Boolean Advanced { get; set; }
public List<SelectOption> SelectOptions { get; set; }
}
}

@ -1,6 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Reflection;
@ -26,13 +28,14 @@ namespace NzbDrone.Api.ClientSchema
if (fieldAttribute != null)
{
var field = new Field()
var field = new Field
{
Name = propertyInfo.Name,
Label = fieldAttribute.Label,
HelpText = fieldAttribute.HelpText,
HelpLink = fieldAttribute.HelpLink,
Order = fieldAttribute.Order,
Advanced = fieldAttribute.Advanced,
Type = fieldAttribute.Type.ToString().ToLowerInvariant()
};
@ -101,6 +104,23 @@ namespace NzbDrone.Api.ClientSchema
propertyInfo.SetValue(target, value, null);
}
else if (propertyInfo.PropertyType == typeof (IEnumerable<Int32>))
{
IEnumerable<Int32> value;
if (field.Value.GetType() == typeof (JArray))
{
value = ((JArray) field.Value).Select(s => s.Value<Int32>());
}
else
{
value = field.Value.ToString().Split(new []{','}, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s));
}
propertyInfo.SetValue(target, value, null);
}
else
{
propertyInfo.SetValue(target, field.Value, null);

@ -39,6 +39,7 @@ namespace NzbDrone.Api.Config
SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 3);
SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat();
SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat();
SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat();
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
}
@ -80,6 +81,7 @@ namespace NzbDrone.Api.Config
var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec);
var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec);
var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec);
var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec);
sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null
? "Invalid format"
@ -93,6 +95,10 @@ namespace NzbDrone.Api.Config
? "Invalid format"
: dailyEpisodeSampleResult.Filename;
sampleResource.AnimeEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult) != null
? "Invalid format"
: animeEpisodeSampleResult.Filename;
sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace()
? "Invalid format"
: _filenameSampleService.GetSeriesFolderSample(nameSpec);

@ -9,6 +9,7 @@ namespace NzbDrone.Api.Config
public Int32 MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; }
public string AnimeEpisodeFormat { get; set; }
public string SeriesFolderFormat { get; set; }
public string SeasonFolderFormat { get; set; }
public bool IncludeSeriesTitle { get; set; }

@ -5,6 +5,7 @@
public string SingleEpisodeExample { get; set; }
public string MultiEpisodeExample { get; set; }
public string DailyEpisodeExample { get; set; }
public string AnimeEpisodeExample { get; set; }
public string SeriesFolderExample { get; set; }
public string SeasonFolderExample { get; set; }
}

@ -19,6 +19,7 @@ namespace NzbDrone.Api.Episodes
public Boolean HasFile { get; set; }
public Boolean Monitored { get; set; }
public Nullable<Int32> SceneAbsoluteEpisodeNumber { get; set; }
public Int32 SceneEpisodeNumber { get; set; }
public Int32 SceneSeasonNumber { get; set; }
public Int32 TvDbEpisodeId { get; set; }

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using FluentValidation;
using Nancy;
using NLog;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
@ -23,18 +25,21 @@ namespace NzbDrone.Api.Indexers
private readonly IMakeDownloadDecision _downloadDecisionMaker;
private readonly IDownloadService _downloadService;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
ISearchForNzb nzbSearchService,
IMakeDownloadDecision downloadDecisionMaker,
IDownloadService downloadService,
IParsingService parsingService)
IParsingService parsingService,
Logger logger)
{
_rssFetcherAndParser = rssFetcherAndParser;
_nzbSearchService = nzbSearchService;
_downloadDecisionMaker = downloadDecisionMaker;
_downloadService = downloadService;
_parsingService = parsingService;
_logger = logger;
GetResourceAll = GetReleases;
Post["/"] = x=> DownloadRelease(this.Bind<ReleaseResource>());
@ -62,9 +67,17 @@ namespace NzbDrone.Api.Indexers
private List<ReleaseResource> GetEpisodeReleases(int episodeId)
{
var decisions = _nzbSearchService.EpisodeSearch(episodeId);
try
{
var decisions = _nzbSearchService.EpisodeSearch(episodeId);
return MapDecisions(decisions);
}
catch (Exception ex)
{
_logger.ErrorException("Episode search failed: " + ex.Message, ex);
}
return MapDecisions(decisions);
return new List<ReleaseResource>();
}
private List<ReleaseResource> GetRss()

@ -15,6 +15,8 @@ namespace NzbDrone.Api.Indexers
public Int64 Size { get; set; }
public String Indexer { get; set; }
public String ReleaseGroup { get; set; }
public String SubGroup { get; set; }
public String ReleaseHash { get; set; }
public String Title { get; set; }
public Boolean FullSeason { get; set; }
public Boolean SceneSource { get; set; }
@ -23,9 +25,10 @@ namespace NzbDrone.Api.Indexers
public String AirDate { get; set; }
public String SeriesTitle { get; set; }
public int[] EpisodeNumbers { get; set; }
public int[] AbsoluteEpisodeNumbers { get; set; }
public Boolean Approved { get; set; }
public Int32 TvRageId { get; set; }
public List<string> Rejections { get; set; }
public IEnumerable<String> Rejections { get; set; }
public DateTime PublishDate { get; set; }
public String CommentUrl { get; set; }
public String DownloadUrl { get; set; }

@ -164,6 +164,7 @@
<Compile Include="Mapping\MappingValidation.cs" />
<Compile Include="Mapping\ResourceMappingException.cs" />
<Compile Include="Mapping\ValueInjectorExtensions.cs" />
<Compile Include="Series\AlternateTitleResource.cs" />
<Compile Include="Update\UpdateResource.cs" />
<Compile Include="Wanted\CutoffModule.cs" />
<Compile Include="Wanted\LegacyMissingModule.cs" />

@ -0,0 +1,10 @@
using System;
namespace NzbDrone.Api.Series
{
public class AlternateTitleResource
{
public String Title { get; set; }
public Int32 SeasonNumber { get; set; }
}
}

@ -15,6 +15,7 @@ using NzbDrone.Api.Mapping;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Core.DataAugmentation.Scene;
using Omu.ValueInjecter;
namespace NzbDrone.Api.Series
{
@ -78,21 +79,6 @@ namespace NzbDrone.Api.Series
PutValidator.RuleFor(s => s.Path).IsValidPath();
}
private void PopulateAlternativeTitles(List<SeriesResource> resources)
{
foreach (var resource in resources)
{
PopulateAlternativeTitles(resource);
}
}
private void PopulateAlternativeTitles(SeriesResource resource)
{
var mapping = _sceneMappingService.FindByTvdbid(resource.TvdbId);
if (mapping == null) return;
resource.AlternativeTitles = mapping.Select(x => x.Title).Distinct().ToList();
}
private SeriesResource GetSeries(int id)
{
var series = _seriesService.GetSeries(id);
@ -106,7 +92,7 @@ namespace NzbDrone.Api.Series
var resource = series.InjectTo<SeriesResource>();
MapCoversToLocal(resource);
FetchAndLinkSeriesStatistics(resource);
PopulateAlternativeTitles(resource);
PopulateAlternateTitles(resource);
return resource;
}
@ -118,7 +104,7 @@ namespace NzbDrone.Api.Series
MapCoversToLocal(seriesResources.ToArray());
LinkSeriesStatistics(seriesResources, seriesStats);
PopulateAlternativeTitles(seriesResources);
PopulateAlternateTitles(seriesResources);
return seriesResources;
}
@ -179,6 +165,23 @@ namespace NzbDrone.Api.Series
resource.NextAiring = seriesStatistics.NextAiring;
}
private void PopulateAlternateTitles(List<SeriesResource> resources)
{
foreach (var resource in resources)
{
PopulateAlternateTitles(resource);
}
}
private void PopulateAlternateTitles(SeriesResource resource)
{
var mappings = _sceneMappingService.FindByTvdbid(resource.TvdbId);
if (mappings == null) return;
resource.AlternateTitles = mappings.InjectTo<List<AlternateTitleResource>>();
}
public void Handle(EpisodeImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId);

@ -15,7 +15,7 @@ namespace NzbDrone.Api.Series
//View Only
public String Title { get; set; }
public List<String> AlternativeTitles { get; set; }
public List<AlternateTitleResource> AlternateTitles { get; set; }
public Int32 SeasonCount
{

@ -22,5 +22,10 @@ namespace NzbDrone.Common
source.Add(item);
}
public static bool Empty<TSource>(this IEnumerable<TSource> source)
{
return !source.Any();
}
}
}

@ -15,16 +15,16 @@ namespace NzbDrone.Common.Reflection
return properties.Where(c => c.PropertyType.IsSimpleType()).ToList();
}
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
{
return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
}
public static bool IsSimpleType(this Type type)
{
if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(Nullable<>) || type.GetGenericTypeDefinition() == typeof(List<>)))
if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(Nullable<>) ||
type.GetGenericTypeDefinition() == typeof(List<>) ||
type.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
type = type.GetGenericArguments()[0];
}

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FizzWare.NBuilder;
using Moq;
@ -14,9 +16,11 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene
public class SceneMappingServiceFixture : CoreTest<SceneMappingService>
{
private List<SceneMapping> _fakeMappings;
private Mock<ISceneMappingProvider> _provider1;
private Mock<ISceneMappingProvider> _provider2;
[SetUp]
public void Setup()
{
@ -33,14 +37,24 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene
_fakeMappings[2].ParseTerm = "Can";
_fakeMappings[3].ParseTerm = "Be";
_fakeMappings[4].ParseTerm = "Cleaned";
}
_provider1 = new Mock<ISceneMappingProvider>();
_provider1.Setup(s => s.GetSceneMappings()).Returns(_fakeMappings);
_provider2 = new Mock<ISceneMappingProvider>();
_provider2.Setup(s => s.GetSceneMappings()).Returns(_fakeMappings);
}
private void GivenProviders(IEnumerable<Mock<ISceneMappingProvider>> providers)
{
Mocker.SetConstant<IEnumerable<ISceneMappingProvider>>(providers.Select(s => s.Object));
}
[Test]
public void UpdateMappings_purge_existing_mapping_and_add_new_ones()
public void should_purge_existing_mapping_and_add_new_ones()
{
Mocker.GetMock<ISceneMappingProxy>().Setup(c => c.Fetch()).Returns(_fakeMappings);
GivenProviders(new [] { _provider1 });
Mocker.GetMock<ISceneMappingRepository>().Setup(c => c.All()).Returns(_fakeMappings);
Subject.Execute(new UpdateSceneMappingCommand());
@ -48,27 +62,26 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene
AssertMappingUpdated();
}
[Test]
public void UpdateMappings_should_not_delete_if_fetch_fails()
public void should_not_delete_if_fetch_fails()
{
GivenProviders(new[] { _provider1 });
Mocker.GetMock<ISceneMappingProxy>().Setup(c => c.Fetch()).Throws(new WebException());
_provider1.Setup(c => c.GetSceneMappings()).Throws(new WebException());
Subject.Execute(new UpdateSceneMappingCommand());
AssertNoUpdate();
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void UpdateMappings_should_not_delete_if_fetch_returns_empty_list()
public void should_not_delete_if_fetch_returns_empty_list()
{
GivenProviders(new[] { _provider1 });
Mocker.GetMock<ISceneMappingProxy>().Setup(c => c.Fetch()).Returns(new List<SceneMapping>());
_provider1.Setup(c => c.GetSceneMappings()).Returns(new List<SceneMapping>());
Subject.Execute(new UpdateSceneMappingCommand());
@ -77,28 +90,37 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_get_mappings_for_all_providers()
{
GivenProviders(new[] { _provider1, _provider2 });
Mocker.GetMock<ISceneMappingRepository>().Setup(c => c.All()).Returns(_fakeMappings);
Subject.Execute(new UpdateSceneMappingCommand());
_provider1.Verify(c => c.GetSceneMappings(), Times.Once());
_provider2.Verify(c => c.GetSceneMappings(), Times.Once());
}
private void AssertNoUpdate()
{
Mocker.GetMock<ISceneMappingProxy>().Verify(c => c.Fetch(), Times.Once());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.Purge(It.IsAny<bool>()), Times.Never());
_provider1.Verify(c => c.GetSceneMappings(), Times.Once());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.Clear(It.IsAny<String>()), Times.Never());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.InsertMany(_fakeMappings), Times.Never());
}
private void AssertMappingUpdated()
{
Mocker.GetMock<ISceneMappingProxy>().Verify(c => c.Fetch(), Times.Once());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.Purge(It.IsAny<bool>()), Times.Once());
_provider1.Verify(c => c.GetSceneMappings(), Times.Once());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.Clear(It.IsAny<String>()), Times.Once());
Mocker.GetMock<ISceneMappingRepository>().Verify(c => c.InsertMany(_fakeMappings), Times.Once());
foreach (var sceneMapping in _fakeMappings)
{
Subject.GetSceneName(sceneMapping.TvdbId).Should().Be(sceneMapping.SearchTerm);
Subject.GetSceneNames(sceneMapping.TvdbId, _fakeMappings.Select(m => m.SeasonNumber)).Should().Contain(sceneMapping.SearchTerm);
Subject.GetTvDbId(sceneMapping.ParseTerm).Should().Be(sceneMapping.TvdbId);
}
}
}
}

@ -1,4 +1,5 @@
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using System;
@ -29,9 +30,9 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer> { indexer.Object });
Mocker.GetMock<NzbDrone.Core.DecisionEngine.IMakeDownloadDecision>()
Mocker.GetMock<DecisionEngine.IMakeDownloadDecision>()
.Setup(s => s.GetSearchDecision(It.IsAny<List<Parser.Model.ReleaseInfo>>(), It.IsAny<SearchCriteriaBase>()))
.Returns(new List<NzbDrone.Core.DecisionEngine.Specifications.DownloadDecision>());
.Returns(new List<DecisionEngine.Specifications.DownloadDecision>());
_xemSeries = Builder<Series>.CreateNew()
.With(v => v.UseSceneNumbering = true)
@ -46,6 +47,10 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Mocker.GetMock<IEpisodeService>()
.Setup(v => v.GetEpisodesBySeason(_xemSeries.Id, It.IsAny<int>()))
.Returns<int, int>((i, j) => _xemEpisodes.Where(d => d.SeasonNumber == j).ToList());
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.GetSceneNames(It.IsAny<Int32>(), It.IsAny<IEnumerable<Int32>>()))
.Returns(new List<String>());
}
private void WithEpisode(int seasonNumber, int episodeNumber, int sceneSeasonNumber, int sceneEpisodeNumber)
@ -90,7 +95,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
private List<SearchCriteriaBase> WatchForSearchCriteria()
{
List<SearchCriteriaBase> result = new List<SearchCriteriaBase>();
var result = new List<SearchCriteriaBase>();
Mocker.GetMock<IFetchFeedFromIndexers>()
.Setup(v => v.Fetch(It.IsAny<IIndexer>(), It.IsAny<SingleEpisodeSearchCriteria>()))
@ -102,6 +107,11 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Callback<IIndexer, SeasonSearchCriteria>((i, s) => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
Mocker.GetMock<IFetchFeedFromIndexers>()
.Setup(v => v.Fetch(It.IsAny<IIndexer>(), It.IsAny<AnimeEpisodeSearchCriteria>()))
.Callback<IIndexer, AnimeEpisodeSearchCriteria>((i, s) => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
return result;
}
@ -186,5 +196,21 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
criteria.Count.Should().Be(1);
criteria[0].SeasonNumber.Should().Be(7);
}
[Test]
public void season_search_for_anime_should_search_for_each_episode()
{
WithEpisodes();
_xemSeries.SeriesType = SeriesTypes.Anime;
var seasonNumber = 1;
var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, seasonNumber);
var criteria = allCriteria.OfType<AnimeEpisodeSearchCriteria>().ToList();
criteria.Count.Should().Be(_xemEpisodes.Count(e => e.SeasonNumber == seasonNumber));
}
}
}

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
@ -12,8 +14,8 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
[TestCase("Franklin & Bash", Result = "Franklin+and+Bash")]
public string should_replace_some_special_characters(string input)
{
Subject.SceneTitle = input;
return Subject.QueryTitle;
Subject.SceneTitles = new List<string> { input };
return Subject.QueryTitles.First();
}
}
}

@ -4,7 +4,6 @@ using FizzWare.NBuilder;
using FluentValidation.Results;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
@ -38,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerTests
indexer.Setup(s => s.Parser.Process(It.IsAny<String>(), It.IsAny<String>()))
.Returns(results);
indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<Int32>(), It.IsAny<Int32>()))
indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny<List<String>>(), It.IsAny<Int32>(), It.IsAny<Int32>(), It.IsAny<Int32>()))
.Returns(new List<string> { "http://www.nzbdrone.com" });
indexer.SetupGet(s => s.SupportedPageSize).Returns(paging ? 100 : 0);
@ -56,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_not_use_offset_if_result_count_is_less_than_90()
{
var indexer = WithIndexer(true, 25);
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title });
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitles = new List<string>{_series.Title} });
Mocker.GetMock<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), Times.Once());
}
@ -65,7 +64,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_not_use_offset_for_sites_that_do_not_support_it()
{
var indexer = WithIndexer(false, 125);
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title });
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitles = new List<string> { _series.Title } });
Mocker.GetMock<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), Times.Once());
}
@ -74,7 +73,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_not_use_offset_if_its_already_tried_10_times()
{
var indexer = WithIndexer(true, 100);
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitle = _series.Title });
Subject.Fetch(indexer, new SeasonSearchCriteria { Series = _series, SceneTitles = new List<string> { _series.Title } });
Mocker.GetMock<IHttpProvider>().Verify(v => v.DownloadString(It.IsAny<String>()), Times.Exactly(10));
}

@ -56,6 +56,14 @@ namespace NzbDrone.Core.Test.MetadataSourceTests
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_have_period_at_start_of_title_slug()
{
var details = Subject.GetSeriesInfo(79099);
details.Item1.TitleSlug.Should().Be("dothack");
}
private void ValidateSeries(Series series)
{
series.Should().NotBeNull();

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.Tvdb;
using NzbDrone.Core.Rest;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
namespace NzbDrone.Core.Test.MetadataSourceTests
{
[TestFixture]
[IntegrationTest]
public class TvdbProxyFixture : CoreTest<TvdbProxy>
{
// [TestCase("The Simpsons", "The Simpsons")]
// [TestCase("South Park", "South Park")]
// [TestCase("Franklin & Bash", "Franklin & Bash")]
// [TestCase("Mr. D", "Mr. D")]
// [TestCase("Rob & Big", "Rob and Big")]
// [TestCase("M*A*S*H", "M*A*S*H")]
// public void successful_search(string title, string expected)
// {
// var result = Subject.SearchForNewSeries(title);
//
// result.Should().NotBeEmpty();
//
// result[0].Title.Should().Be(expected);
// }
//
// [Test]
// public void no_search_result()
// {
// var result = Subject.SearchForNewSeries(Guid.NewGuid().ToString());
// result.Should().BeEmpty();
// }
[TestCase(88031)]
[TestCase(179321)]
public void should_be_able_to_get_series_detail(int tvdbId)
{
var details = Subject.GetSeriesInfo(tvdbId);
//ValidateSeries(details.Item1);
ValidateEpisodes(details.Item2);
}
// [Test]
// public void getting_details_of_invalid_series()
// {
// Assert.Throws<RestException>(() => Subject.GetSeriesInfo(Int32.MaxValue));
//
// ExceptionVerification.ExpectedWarns(1);
// }
//
// [Test]
// public void should_not_have_period_at_start_of_title_slug()
// {
// var details = Subject.GetSeriesInfo(79099);
//
// details.Item1.TitleSlug.Should().Be("dothack");
// }
private void ValidateSeries(Series series)
{
series.Should().NotBeNull();
series.Title.Should().NotBeBlank();
series.CleanTitle.Should().Be(Parser.Parser.CleanSeriesTitle(series.Title));
series.Overview.Should().NotBeBlank();
series.AirTime.Should().NotBeBlank();
series.FirstAired.Should().HaveValue();
series.FirstAired.Value.Kind.Should().Be(DateTimeKind.Utc);
series.Images.Should().NotBeEmpty();
series.ImdbId.Should().NotBeBlank();
series.Network.Should().NotBeBlank();
series.Runtime.Should().BeGreaterThan(0);
series.TitleSlug.Should().NotBeBlank();
series.TvRageId.Should().BeGreaterThan(0);
series.TvdbId.Should().BeGreaterThan(0);
}
private void ValidateEpisodes(List<Episode> episodes)
{
episodes.Should().NotBeEmpty();
episodes.GroupBy(e => e.SeasonNumber.ToString("000") + e.EpisodeNumber.ToString("000"))
.Max(e => e.Count()).Should().Be(1);
episodes.Should().Contain(c => c.SeasonNumber > 0);
// episodes.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Overview));
foreach (var episode in episodes)
{
ValidateEpisode(episode);
//if atleast one episdoe has title it means parse it working.
// episodes.Should().Contain(c => !string.IsNullOrWhiteSpace(c.Title));
}
}
private void ValidateEpisode(Episode episode)
{
episode.Should().NotBeNull();
//TODO: Is there a better way to validate that episode number or season number is greater than zero?
(episode.EpisodeNumber + episode.SeasonNumber).Should().NotBe(0);
episode.Should().NotBeNull();
// if (episode.AirDateUtc.HasValue)
// {
// episode.AirDateUtc.Value.Kind.Should().Be(DateTimeKind.Utc);
// }
}
}
}

@ -173,6 +173,7 @@
<Compile Include="Messaging\Commands\CommandExecutorFixture.cs" />
<Compile Include="Messaging\Commands\CommandFixture.cs" />
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
<Compile Include="MetadataSourceTests\TvdbProxyFixture.cs" />
<Compile Include="MetadataSourceTests\TraktProxyFixture.cs" />
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />
@ -189,6 +190,7 @@
<Compile Include="OrganizerTests\BuildFilePathFixture.cs" />
<Compile Include="OrganizerTests\GetSeriesFolderFixture.cs" />
<Compile Include="ParserTests\AbsoluteEpisodeNumberParserFixture.cs" />
<Compile Include="ParserTests\AnimeMetadataParserFixture.cs" />
<Compile Include="ParserTests\IsPossibleSpecialEpisodeFixture.cs" />
<Compile Include="ParserTests\ReleaseGroupParserFixture.cs" />
<Compile Include="ParserTests\LanguageParserFixture.cs" />
@ -249,7 +251,7 @@
<Compile Include="TvTests\EpisodeProviderTests\EpisodeProviderTest_GetEpisodesByParseResult.cs" />
<Compile Include="FluentTest.cs" />
<Compile Include="InstrumentationTests\DatabaseTargetFixture.cs" />
<Compile Include="OrganizerTests\GetNewFilenameFixture.cs" />
<Compile Include="OrganizerTests\FileNameBuilderFixture.cs" />
<Compile Include="DecisionEngineTests\MonitoredEpisodeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
<Compile Include="Qualities\QualityModelComparerFixture.cs" />

@ -42,12 +42,14 @@ namespace NzbDrone.Core.Test.OrganizerTests
.With(e => e.Title = "City Sushi")
.With(e => e.SeasonNumber = 15)
.With(e => e.EpisodeNumber = 6)
.With(e => e.AbsoluteEpisodeNumber = 100)
.Build();
_episode2 = Builder<Episode>.CreateNew()
.With(e => e.Title = "City Sushi")
.With(e => e.SeasonNumber = 15)
.With(e => e.EpisodeNumber = 7)
.With(e => e.AbsoluteEpisodeNumber = 101)
.Build();
_episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "DRONE" };
@ -435,5 +437,67 @@ namespace NzbDrone.Core.Test.OrganizerTests
Subject.BuildFilename(new List<Episode> { episode }, new Series { Title = "Chicago P.D.." }, _episodeFile)
.Should().Be("Chicago.P.D.S06E06.Part.1");
}
[Test]
public void should_not_replace_absolute_numbering_when_series_is_not_anime()
{
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}";
Subject.BuildFilename(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South.Park.S15E06.City.Sushi");
}
[Test]
public void should_replace_standard_and_absolute_numbering_when_series_is_anime()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.AnimeEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}";
Subject.BuildFilename(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South.Park.S15E06.100.City.Sushi");
}
[Test]
public void should_replace_standard_numbering_when_series_is_anime()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.AnimeEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}";
Subject.BuildFilename(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South.Park.S15E06.City.Sushi");
}
[Test]
public void should_replace_absolute_numbering_when_series_is_anime()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.AnimeEpisodeFormat = "{Series.Title}.{absolute:00}.{Episode.Title}";
Subject.BuildFilename(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("South.Park.100.City.Sushi");
}
[Test]
public void should_replace_multiple_absolute_numbering_when_series_is_anime()
{
_series.SeriesType = SeriesTypes.Anime;
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFilename(new List<Episode> { _episode1, _episode2 }, _series, _episodeFile)
.Should().Be("South Park - 100 - 101 - City Sushi");
}
[Test]
public void should_use_standard_naming_when_anime_episode_has_absolute_number_of_zero()
{
_series.SeriesType = SeriesTypes.Anime;
_episode1.AbsoluteEpisodeNumber = 0;
_namingConfig.StandardEpisodeFormat = "{Series Title} - {season:0}x{episode:00} - {Episode Title}";
_namingConfig.AnimeEpisodeFormat = "{Series Title} - {absolute:000} - {Episode Title}";
Subject.BuildFilename(new List<Episode> { _episode1, }, _series, _episodeFile)
.Should().Be("South Park - 15x06 - City Sushi");
}
}
}

@ -34,6 +34,37 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)]
[TestCase("ducktales_e66_time_is_money_part_one_marking_time", "DuckTales", 66, 0, 0)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)]
[TestCase("[FroZen] Miyuki - 23 [DVD][7F6170E6]", "Miyuki", 23, 0, 0)]
[TestCase("[Commie] Yowamushi Pedal - 32 [0BA19D5B]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[Doki] Mahouka Koukou no Rettousei - 07 (1280x720 Hi10P AAC) [80AF7DDE]", "Mahouka Koukou no Rettousei", 7, 0, 0)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [480p]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]", "Sailor Moon", 4, 0, 0)]
[TestCase("[Chibiki] Puchimas!! - 42 [360p][7A4FC77B]", "Puchimas", 42, 0, 0)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[HorribleSubs] Love Live! S2 - 07 [720p]", "Love Live! S2", 7, 0, 0)]
[TestCase("[DeadFish] Onee-chan ga Kita - 09v2 [720p][AAC]", "Onee-chan ga Kita", 9, 0, 0)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0]", "No Game No Life", 1, 0, 0)]
[TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "Soul Eater Not!", 6, 0, 0)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)]
[TestCase("No Game No Life - 010 (720p) [27AAA0A0].mkv", "No Game No Life", 10, 0, 0)]
[TestCase("Initial D Fifth Stage - 01 DVD - Central Anime", "Initial D Fifth Stage", 1, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_01(DVD)_-_(Central_Anime)[5AF6F1E4].mkv", "Initial D Fifth Stage", 1, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_02(DVD)_-_(Central_Anime)[0CA65F00].mkv", "Initial D Fifth Stage", 2, 0, 0)]
[TestCase("Initial D Fifth Stage - 03 DVD - Central Anime", "Initial D Fifth Stage", 3, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_03(DVD)_-_(Central_Anime)[629BD592].mkv", "Initial D Fifth Stage", 3, 0, 0)]
[TestCase("Initial D Fifth Stage - 14 DVD - Central Anime", "Initial D Fifth Stage", 14, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_14(DVD)_-_(Central_Anime)[0183D922].mkv", "Initial D Fifth Stage", 14, 0, 0)]
// [TestCase("Initial D - 4th Stage Ep 01.mkv", "Initial D - 4th Stage", 1, 0, 0)]
[TestCase("[ChihiroDesuYo].No.Game.No.Life.-.09.1280x720.10bit.AAC.[24CCE81D]", "No.Game.No.Life", 9, 0, 0)]
[TestCase("Fairy Tail - 001 - Fairy Tail", "Fairy Tail", 001, 0, 0)]
[TestCase("Fairy Tail - 049 - The Day of Fated Meeting", "Fairy Tail", 049, 0, 0)]
[TestCase("Fairy Tail - 050 - Special Request Watch Out for the Guy You Like!", "Fairy Tail", 050, 0, 0)]
[TestCase("Fairy Tail - 099 - Natsu vs. Gildarts", "Fairy Tail", 099, 0, 0)]
[TestCase("Fairy Tail - 100 - Mest", "Fairy Tail", 100, 0, 0)]
// [TestCase("Fairy Tail - 101 - Mest", "Fairy Tail", 100, 0, 0)] //This gets caught up in the 'see' numbering
[TestCase("[Exiled-Destiny] Angel Beats Ep01 (D2201EC5).mkv", "Angel Beats!", 1, 0, 0)]
[TestCase("[Commie] Nobunaga the Fool - 23 [5396CA24].mkv", "Nobunaga the Fool", 23, 0, 0)]
[TestCase("[FFF] Seikoku no Dragonar - 01 [1FB538B5].mkv", "Seikoku no Dragonar", 1, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{
var result = Parser.Parser.ParseTitle(postTitle);

@ -0,0 +1,37 @@
using System;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ParserTests
{
[TestFixture]
public class AnimeMetadataParserFixture : CoreTest
{
[TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "SubDESU", "6B7FD717")]
[TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Chihiro", "859EEAFA")]
[TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Underwater", "5C7BC4F9")]
[TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "HorribleSubs", "")]
[TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "HorribleSubs", "")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F]", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")]
[TestCase("[K-F] One Piece 214", "K-F", "")]
[TestCase("[K-F] One Piece S10E14 214", "K-F", "")]
[TestCase("[K-F] One Piece 10x14 214", "K-F", "")]
[TestCase("[K-F] One Piece 214 10x14", "K-F", "")]
[TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")]
[TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")]
[TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")]
public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash)
{
var result = Parser.Parser.ParseTitle(postTitle);
result.Should().NotBeNull();
result.ReleaseGroup.Should().Be(subGroup);
result.ReleaseHash.Should().Be(hash);
}
}
}

@ -52,6 +52,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sonny.With.a.Chance.S02E15.divx", false)]
[TestCase("The.Girls.Next.Door.S03E06.HDTV-WiDE", false)]
[TestCase("Degrassi.S10E27.WS.DSR.XviD-2HD", false)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [480p]", false)]
[TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]", false)]
[TestCase("[Hatsuyuki] Naruto Shippuuden - 363 [848x480][ADE35E38]", false)]
public void should_parse_sdtv_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.SDTV, proper);
@ -69,8 +72,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Girls.Next.Door.S03E06.DVD.Rip.XviD-WiDE", false)]
[TestCase("the.shield.1x13.circles.ws.xvidvd-tns", false)]
[TestCase("the_x-files.9x18.sunshine_days.ac3.ws_dvdrip_xvid-fov.avi", false)]
[TestCase("[FroZen] Miyuki - 23 [DVD][7F6170E6]", false)]
[TestCase("Hannibal.S01E05.576p.BluRay.DD5.1.x264-HiSD", false)]
[TestCase("Hannibal.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)]
[TestCase("Heidi Girl of the Alps (BD)(640x480(RAW) (BATCH 1) (1-13)", false)]
public void should_parse_dvd_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.DVD, proper);
@ -96,6 +101,11 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sonny.With.a.Chance.S02E15.mkv", false)]
[TestCase(@"E:\Downloads\tv\The.Big.Bang.Theory.S01E01.720p.HDTV\ajifajjjeaeaeqwer_eppj.avi", false)]
[TestCase("Gem.Hunt.S01E08.Tourmaline.Nepal.720p.HDTV.x264-DHD", false)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0]", false)]
[TestCase("[Doki] Mahouka Koukou no Rettousei - 07 (1280x720 Hi10P AAC) [80AF7DDE]", false)]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", false)]
[TestCase("[HorribleSubs]_Fairy_Tail_-_145_[720p]", false)]
[TestCase("[Eveyuu] No Game No Life - 10 [Hi10P 1280x720 H264][10B23BD8]", false)]
public void should_parse_hdtv720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.HDTV720p, proper);
@ -106,6 +116,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("DEXTER.S07E01.ARE.YOU.1080P.HDTV.x264-QCF", false)]
[TestCase("DEXTER.S07E01.ARE.YOU.1080P.HDTV.proper.X264-QCF", true)]
[TestCase("Dexter - S01E01 - Title [HDTV-1080p]", false)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", false)]
public void should_parse_hdtv1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.HDTV1080p, proper);
@ -144,6 +155,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Chuck - S01E03 - Come Fly With Me - 720p BluRay.mkv", false)]
[TestCase("The Big Bang Theory.S03E01.The Electric Can Opener Fluctuation.m2ts", false)]
[TestCase("Revolution.S01E02.Chained.Heat.[Bluray720p].mkv", false)]
[TestCase("[FFF] DATE A LIVE - 01 [BD][720p-AAC][0601BED4]", false)]
[TestCase("[coldhell] Pupa v3 [BD720p][03192D4C]", false)]
[TestCase("[RandomRemux] Nobunagun - 01 [720p BD][043EA407].mkv", false)]
[TestCase("[Kaylith] Isshuukan Friends Specials - 01 [BD 720p AAC][B7EEE164].mkv", false)]
public void should_parse_bluray720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
@ -152,6 +167,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Chuck - S01E03 - Come Fly With Me - 1080p BluRay.mkv", false)]
[TestCase("Sons.Of.Anarchy.S02E13.1080p.BluRay.x264-AVCDVD", false)]
[TestCase("Revolution.S01E02.Chained.Heat.[Bluray1080p].mkv", false)]
[TestCase("[FFF] Namiuchigiwa no Muromi-san - 10 [BD][1080p-FLAC][0C4091AF]", false)]
[TestCase("[coldhell] Pupa v2 [BD1080p][5A45EABE].mkv", false)]
[TestCase("[Kaylith] Isshuukan Friends Specials - 01 [BD 1080p FLAC][429FD8C7].mkv", false)]
[TestCase("[Zurako] Log Horizon - 01 - The Apocalypse (BD 1080p AAC) [7AE12174].mkv", false)]
public void should_parse_bluray1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray1080p, proper);

@ -157,8 +157,22 @@ namespace NzbDrone.Core.Test.TvTests
}
[Test]
public void should_not_set_absolute_episode_number_for_non_anime()
{
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
.Returns(new List<Episode>());
Subject.RefreshEpisodeInfo(GetSeries(), GetEpisodes());
_insertedEpisodes.All(e => e.AbsoluteEpisodeNumber == 0).Should().BeTrue();
}
[Test]
[Ignore]
public void should_set_absolute_episode_number()
{
//TODO: Only run this against an anime series
Mocker.GetMock<IEpisodeService>().Setup(c => c.GetEpisodeBySeries(It.IsAny<int>()))
.Returns(new List<Episode>());

@ -10,11 +10,12 @@ namespace NzbDrone.Core.Annotations
Order = order;
}
public int Order { get; private set; }
public string Label { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public Int32 Order { get; private set; }
public String Label { get; set; }
public String HelpText { get; set; }
public String HelpLink { get; set; }
public FieldType Type { get; set; }
public Boolean Advanced { get; set; }
public Type SelectOptions { get; set; }
}

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.DataAugmentation.Scene
{
public interface ISceneMappingProvider
{
List<SceneMapping> GetSceneMappings();
}
}

@ -16,5 +16,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene
[JsonProperty("season")]
public int SeasonNumber { get; set; }
public string Type { get; set; }
}
}

@ -1,3 +1,4 @@
using System;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using System.Collections.Generic;
@ -8,6 +9,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public interface ISceneMappingRepository : IBasicRepository<SceneMapping>
{
List<SceneMapping> FindByTvdbid(int tvdbId);
void Clear(string type);
}
public class SceneMappingRepository : BasicRepository<SceneMapping>, ISceneMappingRepository
@ -21,5 +23,10 @@ namespace NzbDrone.Core.DataAugmentation.Scene
{
return Query.Where(x => x.TvdbId == tvdbId);
}
public void Clear(string type)
{
Delete(s => s.Type == type);
}
}
}

@ -1,6 +1,7 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
@ -12,45 +13,52 @@ namespace NzbDrone.Core.DataAugmentation.Scene
{
public interface ISceneMappingService
{
string GetSceneName(int tvdbId);
Nullable<int> GetTvDbId(string cleanName);
List<String> GetSceneNames(int tvdbId, IEnumerable<Int32> seasonNumbers);
Nullable<int> GetTvDbId(string title);
List<SceneMapping> FindByTvdbid(int tvdbId);
Nullable<Int32> GetSeasonNumber(string title);
}
public class SceneMappingService : ISceneMappingService,
IHandleAsync<ApplicationStartedEvent>,
IExecute<UpdateSceneMappingCommand>
IHandleAsync<ApplicationStartedEvent>,
IExecute<UpdateSceneMappingCommand>
{
private readonly ISceneMappingRepository _repository;
private readonly ISceneMappingProxy _sceneMappingProxy;
private readonly IEnumerable<ISceneMappingProvider> _sceneMappingProviders;
private readonly Logger _logger;
private readonly ICached<SceneMapping> _getSceneNameCache;
private readonly ICached<SceneMapping> _gettvdbIdCache;
private readonly ICached<List<SceneMapping>> _findbytvdbIdCache;
public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ICacheManager cacheManager, Logger logger)
public SceneMappingService(ISceneMappingRepository repository,
ICacheManager cacheManager,
IEnumerable<ISceneMappingProvider> sceneMappingProviders,
Logger logger)
{
_repository = repository;
_sceneMappingProxy = sceneMappingProxy;
_sceneMappingProviders = sceneMappingProviders;
_getSceneNameCache = cacheManager.GetCache<SceneMapping>(GetType(), "scene_name");
_gettvdbIdCache = cacheManager.GetCache<SceneMapping>(GetType(), "tvdb_id");
_findbytvdbIdCache = cacheManager.GetCache<List<SceneMapping>>(GetType(), "find_tvdb_id");
_logger = logger;
}
public string GetSceneName(int tvdbId)
public List<String> GetSceneNames(int tvdbId, IEnumerable<Int32> seasonNumbers)
{
var mapping = _getSceneNameCache.Find(tvdbId.ToString());
var names = _findbytvdbIdCache.Find(tvdbId.ToString());
if (mapping == null) return null;
if (names == null)
{
return new List<String>();
}
return mapping.SearchTerm;
return FilterNonEnglish(names.Where(s => seasonNumbers.Contains(s.SeasonNumber) ||
s.SeasonNumber == -1)
.Select(m => m.SearchTerm).Distinct().ToList());
}
public Nullable<Int32> GetTvDbId(string cleanName)
public Nullable<Int32> GetTvDbId(string title)
{
var mapping = _gettvdbIdCache.Find(cleanName.CleanSeriesTitle());
var mapping = _gettvdbIdCache.Find(title.CleanSeriesTitle());
if (mapping == null)
return null;
@ -60,60 +68,87 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public List<SceneMapping> FindByTvdbid(int tvdbId)
{
return _findbytvdbIdCache.Find(tvdbId.ToString());
var mappings = _findbytvdbIdCache.Find(tvdbId.ToString());
if (mappings == null)
{
return new List<SceneMapping>();
}
return mappings;
}
public Nullable<Int32> GetSeasonNumber(string title)
{
//TODO: we should be able to override xem aliases with ones from services
//Example Fairy Tail - Alias is assigned to season 2 (anidb), but we're still using tvdb for everything
var mapping = _gettvdbIdCache.Find(title.CleanSeriesTitle());
if (mapping == null)
return null;
return mapping.SeasonNumber;
}
private void UpdateMappings()
{
_logger.Info("Updating Scene mapping");
_logger.Info("Updating Scene mappings");
try
foreach (var sceneMappingProvider in _sceneMappingProviders)
{
var mappings = _sceneMappingProxy.Fetch();
if (mappings.Any())
try
{
_repository.Purge();
var mappings = sceneMappingProvider.GetSceneMappings();
foreach (var sceneMapping in mappings)
if (mappings.Any())
{
sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle();
}
_repository.Clear(sceneMappingProvider.GetType().Name);
foreach (var sceneMapping in mappings)
{
sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle();
sceneMapping.Type = sceneMappingProvider.GetType().Name;
}
_repository.InsertMany(mappings);
_repository.InsertMany(mappings.DistinctBy(s => s.ParseTerm).ToList());
}
else
{
_logger.Warn("Received empty list of mapping. will not update.");
}
}
else
catch (Exception ex)
{
_logger.Warn("Received empty list of mapping. will not update.");
_logger.ErrorException("Failed to Update Scene Mappings:", ex);
}
}
catch (Exception ex)
{
_logger.ErrorException("Failed to Update Scene Mappings:", ex);
}
RefreshCache();
}
private void RefreshCache()
{
var mappings = _repository.All();
var mappings = _repository.All().ToList();
_gettvdbIdCache.Clear();
_getSceneNameCache.Clear();
_findbytvdbIdCache.Clear();
foreach (var sceneMapping in mappings)
{
_getSceneNameCache.Set(sceneMapping.TvdbId.ToString(), sceneMapping);
_gettvdbIdCache.Set(sceneMapping.ParseTerm.CleanSeriesTitle(), sceneMapping);
}
foreach (var sceneMapping in mappings.GroupBy(x => x.TvdbId))
{
_findbytvdbIdCache.Set(sceneMapping.Key.ToString(), sceneMapping.ToList());
}
}
private List<String> FilterNonEnglish(List<String> titles)
{
return titles.Where(title => title.All(c => c <= 255)).ToList();
}
public void HandleAsync(ApplicationStartedEvent message)
{

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.DataAugmentation.Scene
{
public class ServicesProvider : ISceneMappingProvider
{
private readonly ISceneMappingProxy _sceneMappingProxy;
public ServicesProvider(ISceneMappingProxy sceneMappingProxy)
{
_sceneMappingProxy = sceneMappingProxy;
}
public List<SceneMapping> GetSceneMappings()
{
return _sceneMappingProxy.Fetch();
}
}
}

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DataAugmentation.Xem.Model;
using NzbDrone.Core.Rest;
using RestSharp;
@ -12,6 +14,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem
{
List<int> GetXemSeriesIds();
List<XemSceneTvdbMapping> GetSceneTvdbMappings(int id);
List<SceneMapping> GetSceneTvdbNames();
}
public class XemProxy : IXemProxy
@ -65,6 +68,47 @@ namespace NzbDrone.Core.DataAugmentation.Xem
return response.Data.Where(c => c.Scene != null).ToList();
}
public List<SceneMapping> GetSceneTvdbNames()
{
_logger.Debug("Fetching alternate names");
var restClient = new RestClient(XEM_BASE_URL);
var request = BuildRequest("allNames");
request.AddParameter("origin", "tvdb");
//request.AddParameter("language", "us");
request.AddParameter("seasonNumbers", true);
var response = restClient.ExecuteAndValidate<XemResult<Dictionary<Int32, List<JObject>>>>(request);
CheckForFailureResult(response);
var result = new List<SceneMapping>();
foreach (var series in response.Data)
{
foreach (var name in series.Value)
{
foreach (var n in name)
{
int seasonNumber;
if (!Int32.TryParse(n.Value.ToString(), out seasonNumber))
{
continue;
}
result.Add(new SceneMapping
{
Title = n.Key,
SearchTerm = n.Key,
SeasonNumber = seasonNumber,
TvdbId = series.Key
});
}
}
}
return result;
}
private static void CheckForFailureResult<T>(XemResult<T> response)
{
if (response.Result.Equals("failure", StringComparison.InvariantCultureIgnoreCase) &&
@ -73,7 +117,5 @@ namespace NzbDrone.Core.DataAugmentation.Xem
throw new Exception("Error response received from Xem: " + response.Message);
}
}
}
}

@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.DataAugmentation.Xem
{
public class XemService : IHandle<SeriesUpdatedEvent>, IHandle<SeriesRefreshStartingEvent>
public class XemService : ISceneMappingProvider, IHandle<SeriesUpdatedEvent>, IHandle<SeriesRefreshStartingEvent>
{
private readonly IEpisodeService _episodeService;
private readonly IXemProxy _xemProxy;
@ -47,7 +49,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem
foreach (var episode in episodes)
{
episode.AbsoluteEpisodeNumber = 0;
episode.SceneAbsoluteEpisodeNumber = 0;
episode.SceneSeasonNumber = 0;
episode.SceneEpisodeNumber = 0;
}
@ -64,7 +66,7 @@ namespace NzbDrone.Core.DataAugmentation.Xem
continue;
}
episode.AbsoluteEpisodeNumber = mapping.Scene.Absolute;
episode.SceneAbsoluteEpisodeNumber = mapping.Scene.Absolute;
episode.SceneSeasonNumber = mapping.Scene.Season;
episode.SceneEpisodeNumber = mapping.Scene.Episode;
}
@ -96,6 +98,24 @@ namespace NzbDrone.Core.DataAugmentation.Xem
}
}
public List<SceneMapping> GetSceneMappings()
{
var mappings = _xemProxy.GetSceneTvdbNames();
return mappings.Where(m =>
{
int id;
if (Int32.TryParse(m.Title, out id))
{
_logger.Debug("Skipping all numeric name: {0} for {1}", m.Title, m.TvdbId);
return false;
}
return true;
}).ToList();
}
public void Handle(SeriesUpdatedEvent message)
{
if (_cache.Count == 0)

@ -0,0 +1,20 @@
using NzbDrone.Core.Datastore.Migration.Framework;
using FluentMigrator;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(52)]
public class add_columns_for_anime : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
//Support XEM names
Alter.Table("SceneMappings").AddColumn("Type").AsString().Nullable();
Execute.Sql("DELETE FROM SceneMappings");
//Add AnimeEpisodeFormat (set to Stardard Episode format for now)
Alter.Table("NamingConfig").AddColumn("AnimeEpisodeFormat").AsString().Nullable();
Execute.Sql("UPDATE NamingConfig SET AnimeEpisodeFormat = StandardEpisodeFormat");
}
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
@ -136,6 +137,11 @@ namespace NzbDrone.Core.History
history.Data.Add("DownloadClientId", message.DownloadClientId);
}
if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
{
history.Data.Add("ReleaseHash", message.Episode.ParsedEpisodeInfo.ReleaseHash);
}
_historyRepository.Insert(history);
}
}

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Newznab;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class UpdateAnimeCategories : IHousekeepingTask
{
private readonly IIndexerFactory _indexerFactory;
private readonly Logger _logger;
private const int NZBS_ORG_ANIME_ID = 7040;
private const int NEWZNAB_ANIME_ID = 5070;
public UpdateAnimeCategories(IIndexerFactory indexerFactory, Logger logger)
{
_indexerFactory = indexerFactory;
_logger = logger;
}
public void Clean()
{
//TODO: We should remove this before merging it into develop
_logger.Debug("Updating Anime Categories for newznab indexers");
var indexers = _indexerFactory.All().Where(i => i.Implementation == typeof (Newznab).Name);
foreach (var indexer in indexers)
{
var settings = indexer.Settings as NewznabSettings;
if (settings.Url.ContainsIgnoreCase("nzbs.org") && settings.Categories.Contains(NZBS_ORG_ANIME_ID))
{
var animeCategories = new List<int>(settings.AnimeCategories);
animeCategories.Add(NZBS_ORG_ANIME_ID);
settings.AnimeCategories = animeCategories;
settings.Categories = settings.Categories.Where(c => c != NZBS_ORG_ANIME_ID);
indexer.Settings = settings;
_indexerFactory.Update(indexer);
}
else if (settings.Categories.Contains(NEWZNAB_ANIME_ID))
{
var animeCategories = new List<int>(settings.AnimeCategories);
animeCategories.Add(NEWZNAB_ANIME_ID);
settings.AnimeCategories = animeCategories;
settings.Categories = settings.Categories.Where(c => c != NEWZNAB_ANIME_ID);
indexer.Settings = settings;
_indexerFactory.Update(indexer);
}
}
}
}
}

@ -8,7 +8,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
return string.Format("[{0} : {1:00}]", SceneTitle, AbsoluteEpisodeNumber);
return string.Format("[{0} : {1:00}]", Series.Title, AbsoluteEpisodeNumber);
}
}
}

@ -8,7 +8,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
return string.Format("[{0} : {1}", SceneTitle, AirDate);
return string.Format("[{0} : {1}", Series.Title, AirDate);
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.Tv;
@ -12,14 +13,14 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public Series Series { get; set; }
public string SceneTitle { get; set; }
public List<String> SceneTitles { get; set; }
public List<Episode> Episodes { get; set; }
public string QueryTitle
public List<String> QueryTitles
{
get
{
return GetQueryTitle(SceneTitle);
return SceneTitles.Select(GetQueryTitle).ToList();
}
}

@ -6,7 +6,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
return string.Format("[{0} : S{1:00}]", SceneTitle, SeasonNumber);
return string.Format("[{0} : S{1:00}]", Series.Title, SeasonNumber);
}
}
}

@ -7,7 +7,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
return string.Format("[{0} : S{1:00}E{2:00}]", SceneTitle, SeasonNumber, EpisodeNumber);
return string.Format("[{0} : S{1:00}E{2:00}]", Series.Title, SeasonNumber, EpisodeNumber);
}
}
}

@ -11,7 +11,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public override string ToString()
{
return string.Format("[{0} : {1}]", SceneTitle, String.Join(",", EpisodeQueryTitles));
return string.Format("[{0} : {1}]", Series.Title, String.Join(",", EpisodeQueryTitles));
}
}
}

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.Remoting.Messaging;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
@ -84,6 +86,71 @@ namespace NzbDrone.Core.IndexerSearch
return SearchSingle(series, episode);
}
public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber)
{
var series = _seriesService.GetSeries(seriesId);
var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber);
if (series.SeriesType == SeriesTypes.Anime)
{
return SearchAnimeSeason(series, episodes);
}
if (seasonNumber == 0)
{
// search for special episodes in season 0
return SearchSpecial(series, episodes);
}
var downloadDecisions = new List<DownloadDecision>();
if (series.UseSceneNumbering)
{
var sceneSeasonGroups = episodes.GroupBy(v =>
{
if (v.SceneSeasonNumber == 0 && v.SceneEpisodeNumber == 0)
return v.SeasonNumber;
else
return v.SceneSeasonNumber;
}).Distinct();
foreach (var sceneSeasonEpisodes in sceneSeasonGroups)
{
if (sceneSeasonEpisodes.Count() == 1)
{
var episode = sceneSeasonEpisodes.First();
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, sceneSeasonEpisodes.ToList());
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
if (episode.SceneSeasonNumber == 0 && episode.SceneEpisodeNumber == 0)
searchSpec.EpisodeNumber = episode.EpisodeNumber;
else
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber;
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
else
{
var searchSpec = Get<SeasonSearchCriteria>(series, sceneSeasonEpisodes.ToList());
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
}
}
else
{
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
searchSpec.SeasonNumber = seasonNumber;
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
return downloadDecisions;
}
private List<DownloadDecision> SearchSingle(Series series, Episode episode)
{
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, new List<Episode>{episode});
@ -123,10 +190,17 @@ namespace NzbDrone.Core.IndexerSearch
private List<DownloadDecision> SearchAnime(Series series, Episode episode)
{
var searchSpec = Get<AnimeEpisodeSearchCriteria>(series, new List<Episode> { episode });
// TODO: Get the scene title from TheXEM
searchSpec.SceneTitle = series.Title;
// TODO: Calculate the Absolute Episode Number on the fly (if I have to)
searchSpec.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber.GetValueOrDefault(0);
searchSpec.AbsoluteEpisodeNumber = episode.SceneAbsoluteEpisodeNumber.GetValueOrDefault(0);
if (searchSpec.AbsoluteEpisodeNumber == 0)
{
searchSpec.AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber.GetValueOrDefault(0);
}
if (searchSpec.AbsoluteEpisodeNumber == 0)
{
throw new ArgumentOutOfRangeException("AbsoluteEpisodeNumber", "Can not search for an episode absolute episode number of zero");
}
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
}
@ -136,67 +210,19 @@ namespace NzbDrone.Core.IndexerSearch
var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes);
// build list of queries for each episode in the form: "<series> <episode-title>"
searchSpec.EpisodeQueryTitles = episodes.Where(e => !String.IsNullOrWhiteSpace(e.Title))
.Select(e => searchSpec.QueryTitle + " " + SearchCriteriaBase.GetQueryTitle(e.Title))
.SelectMany(e => searchSpec.QueryTitles.Select(title => title + " " + SearchCriteriaBase.GetQueryTitle(e.Title)))
.ToArray();
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
}
public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber)
private List<DownloadDecision> SearchAnimeSeason(Series series, List<Episode> episodes)
{
var series = _seriesService.GetSeries(seriesId);
var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber);
if (seasonNumber == 0)
{
// search for special episodes in season 0
return SearchSpecial(series, episodes);
}
List<DownloadDecision> downloadDecisions = new List<DownloadDecision>();
if (series.UseSceneNumbering)
{
var sceneSeasonGroups = episodes.GroupBy(v =>
{
if (v.SceneSeasonNumber == 0 && v.SceneEpisodeNumber == 0)
return v.SeasonNumber;
else
return v.SceneSeasonNumber;
}).Distinct();
foreach (var sceneSeasonEpisodes in sceneSeasonGroups)
{
if (sceneSeasonEpisodes.Count() == 1)
{
var episode = sceneSeasonEpisodes.First();
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, sceneSeasonEpisodes.ToList());
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
if (episode.SceneSeasonNumber == 0 && episode.SceneEpisodeNumber == 0)
searchSpec.EpisodeNumber = episode.EpisodeNumber;
else
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber;
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
else
{
var searchSpec = Get<SeasonSearchCriteria>(series, sceneSeasonEpisodes.ToList());
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
var downloadDecisions = new List<DownloadDecision>();
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
}
}
else
foreach (var episode in episodes)
{
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
searchSpec.SeasonNumber = seasonNumber;
var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
downloadDecisions.AddRange(SearchAnime(series, episode));
}
return downloadDecisions;
@ -207,13 +233,14 @@ namespace NzbDrone.Core.IndexerSearch
var spec = new TSpec();
spec.Series = series;
spec.SceneTitle = _sceneMapping.GetSceneName(series.TvdbId);
spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber)
.Concat(episodes.Select(e => e.SceneSeasonNumber)
.Distinct()));
spec.Episodes = episodes;
if (string.IsNullOrWhiteSpace(spec.SceneTitle))
{
spec.SceneTitle = series.Title;
}
spec.SceneTitles.Add(series.Title);
return spec;
}

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Animezb
{
public class Animezb : IndexerBase<NullConfig>
{
private static readonly Regex RemoveCharactersRegex = new Regex(@"[!?`]", RegexOptions.Compiled);
private static readonly Regex RemoveSingleCharacterRegex = new Regex(@"\b[a-z0-9]\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex DuplicateCharacterRegex = new Regex(@"[ +]{2,}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override DownloadProtocol Protocol
{
get
{
return DownloadProtocol.Usenet;
}
}
public override bool SupportsSearching
{
get
{
return true;
}
}
public override IParseFeed Parser
{
get
{
return new AnimezbParser();
}
}
public override IEnumerable<string> RecentFeed
{
get
{
yield return "https://animezb.com/rss?cat=anime&max=100";
}
}
public override IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber)
{
return new List<string>();
}
public override IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset)
{
return new List<string>();
}
public override IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
{
return new List<string>();
}
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
{
return titles.SelectMany(title =>
RecentFeed.Select(url =>
String.Format("{0}&q={1}", url, GetSearchQuery(title, absoluteEpisodeNumber))));
}
public override IEnumerable<string> GetSearchUrls(string query, int offset)
{
return new List<string>();
}
private String GetSearchQuery(string title, int absoluteEpisodeNumber)
{
var match = RemoveSingleCharacterRegex.Match(title);
if (match.Success)
{
title = RemoveSingleCharacterRegex.Replace(title, "");
//Since we removed a character we need to not wrap it in quotes and hope animedb doesn't give us a million results
return CleanTitle(String.Format("{0}+{1:00}", title, absoluteEpisodeNumber));
}
//Wrap the query in quotes and search!
return CleanTitle(String.Format("\"{0}+{1:00}\"", title, absoluteEpisodeNumber));
}
private String CleanTitle(String title)
{
title = RemoveCharactersRegex.Replace(title, "");
return DuplicateCharacterRegex.Replace(title, "+");
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace NzbDrone.Core.Indexers.Animezb
{
public class AnimezbParser : RssParserBase
{
protected override string GetNzbInfoUrl(XElement item)
{
IEnumerable<XElement> matches = item.DescendantsAndSelf("link");
if (matches.Any())
{
return matches.First().Value;
}
return String.Empty;
}
protected override long GetSize(XElement item)
{
IEnumerable<XElement> matches = item.DescendantsAndSelf("enclosure");
if (matches.Any())
{
XElement enclosureElement = matches.First();
return Convert.ToInt64(enclosureElement.Attribute("length").Value);
}
return 0;
}
}
}

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Fanzub
{
public class Fanzub : IndexerBase<NullConfig>
{
private static readonly Regex RemoveCharactersRegex = new Regex(@"[!?`]", RegexOptions.Compiled);
public override DownloadProtocol Protocol
{
get
@ -15,14 +18,6 @@ namespace NzbDrone.Core.Indexers.Fanzub
}
}
public override bool SupportsPaging
{
get
{
return false;
}
}
public override bool SupportsSearching
{
get
@ -43,33 +38,47 @@ namespace NzbDrone.Core.Indexers.Fanzub
{
get
{
yield return "http://fanzub.com/rss/?cat=anime";
yield return "https://fanzub.com/rss/?cat=anime&max=100";
}
}
public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber)
public override IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber)
{
return new List<string>();
}
public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset)
public override IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset)
{
return new List<string>();
}
public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
public override IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
{
return new List<string>();
}
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber)
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
{
return RecentFeed.Select(url => String.Format("{0}&q={1}%20{2}", url, seriesTitle, absoluteEpisodeNumber));
return RecentFeed.Select(url => String.Format("{0}&q={1}",
url,
String.Join("|", titles.SelectMany(title => GetTitleSearchStrings(title, absoluteEpisodeNumber)))));
}
public override IEnumerable<string> GetSearchUrls(string query, int offset)
{
return new List<string>();
}
private IEnumerable<String> GetTitleSearchStrings(string title, int absoluteEpisodeNumber)
{
var formats = new[] { "{0}%20{1:00}", "{0}%20-%20{1:00}" };
return formats.Select(s => "\"" + String.Format(s, CleanTitle(title), absoluteEpisodeNumber) + "\"" );
}
private String CleanTitle(String title)
{
return RemoveCharactersRegex.Replace(title, "");
}
}
}

@ -13,10 +13,10 @@ namespace NzbDrone.Core.Indexers
Boolean SupportsSearching { get; }
IEnumerable<string> RecentFeed { get; }
IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber);
IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber);
IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date);
IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber);
IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset);
IEnumerable<string> GetSearchUrls(string query, int offset = 0);
}
}

@ -50,10 +50,10 @@ namespace NzbDrone.Core.Indexers
public virtual IParseFeed Parser { get; private set; }
public abstract IEnumerable<string> RecentFeed { get; }
public abstract IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
public abstract IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
public abstract IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber);
public abstract IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
public abstract IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber);
public abstract IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date);
public abstract IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber);
public abstract IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset);
public abstract IEnumerable<string> GetSearchUrls(string query, int offset);
public override string ToString()

@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers
{
_logger.Debug("Searching for {0} offset: {1}", searchCriteria, offset);
var searchUrls = indexer.GetSeasonSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, offset);
var searchUrls = indexer.GetSeasonSearchUrls(searchCriteria.QueryTitles, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, offset);
var result = Fetch(indexer, searchUrls);
_logger.Info("{0} offset {1}. Found {2}", indexer, searchCriteria, result.Count);
@ -76,7 +76,7 @@ namespace NzbDrone.Core.Indexers
{
_logger.Debug("Searching for {0}", searchCriteria);
var searchUrls = indexer.GetEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
var searchUrls = indexer.GetEpisodeSearchUrls(searchCriteria.QueryTitles, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
var result = Fetch(indexer, searchUrls);
_logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
@ -87,7 +87,7 @@ namespace NzbDrone.Core.Indexers
{
_logger.Debug("Searching for {0}", searchCriteria);
var searchUrls = indexer.GetDailyEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.AirDate);
var searchUrls = indexer.GetDailyEpisodeSearchUrls(searchCriteria.QueryTitles, searchCriteria.Series.TvRageId, searchCriteria.AirDate);
var result = Fetch(indexer, searchUrls);
_logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
@ -98,7 +98,7 @@ namespace NzbDrone.Core.Indexers
{
_logger.Debug("Searching for {0}", searchCriteria);
var searchUrls = indexer.GetAnimeEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.AbsoluteEpisodeNumber);
var searchUrls = indexer.GetAnimeEpisodeSearchUrls(searchCriteria.SceneTitles, searchCriteria.Series.TvRageId, searchCriteria.AbsoluteEpisodeNumber);
var result = Fetch(indexer, searchUrls);
_logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using NzbDrone.Common;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers.Newznab
@ -79,13 +81,9 @@ namespace NzbDrone.Core.Indexers.Newznab
{
get
{
//Todo: We should be able to update settings on start
if (Settings.Url.Contains("nzbs.org"))
{
Settings.Categories = new List<int> { 5000 };
}
var categories = String.Join(",", Settings.Categories.Concat(Settings.AnimeCategories));
var url = String.Format("{0}/api?t=tvsearch&cat={1}&extended=1", Settings.Url.TrimEnd('/'), String.Join(",", Settings.Categories));
var url = String.Format("{0}/api?t=tvsearch&cat={1}&extended=1{2}", Settings.Url.TrimEnd('/'), categories, Settings.AdditionalParameters);
if (!String.IsNullOrWhiteSpace(Settings.ApiKey))
{
@ -96,49 +94,79 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber)
public override IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber)
{
if (Settings.Categories.Empty())
{
return Enumerable.Empty<String>();
}
if (tvRageId > 0)
{
return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&ep={3}", url, tvRageId, seasonNumber, episodeNumber));
}
return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, episodeNumber));
return titles.SelectMany(title =>
RecentFeed.Select(url =>
String.Format("{0}&limit=100&q={1}&season={2}&ep={3}",
url, NewsnabifyTitle(title), seasonNumber, episodeNumber)));
}
public override IEnumerable<string> GetSearchUrls(string query, int offset)
public override IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
{
// encode query (replace the + with spaces first)
query = query.Replace("+", " ");
query = System.Web.HttpUtility.UrlEncode(query);
return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query));
}
if (Settings.Categories.Empty())
{
return Enumerable.Empty<String>();
}
public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
{
if (tvRageId > 0)
{
return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, tvRageId, date)).ToList();
}
return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, NewsnabifyTitle(seriesTitle), date)).ToList();
return titles.SelectMany(title =>
RecentFeed.Select(url =>
String.Format("{0}&limit=100&q={1}&season={2:yyyy}&ep={2:MM}/{2:dd}",
url, NewsnabifyTitle(title), date)).ToList());
}
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber)
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
{
// TODO: Implement
return new List<string>();
if (Settings.AnimeCategories.Empty())
{
return Enumerable.Empty<String>();
}
return titles.SelectMany(title =>
RecentFeed.Select(url =>
String.Format("{0}&limit=100&q={1}+{2:00}",
url.Replace("t=tvsearch", "t=search"), NewsnabifyTitle(title), absoluteEpisodeNumber)));
}
public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset)
public override IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset)
{
if (Settings.Categories.Empty())
{
return Enumerable.Empty<String>();
}
if (tvRageId > 0)
{
return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&offset={3}", url, tvRageId, seasonNumber, offset));
}
return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset));
return titles.SelectMany(title =>
RecentFeed.Select(url =>
String.Format("{0}&limit=100&q={1}&season={2}&offset={3}",
url, NewsnabifyTitle(title), seasonNumber, offset)));
}
public override IEnumerable<string> GetSearchUrls(string query, int offset)
{
// encode query (replace the + with spaces first)
query = query.Replace("+", " ");
query = System.Web.HttpUtility.UrlEncode(query);
return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query));
}
private static string NewsnabifyTitle(string title)

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation;
using FluentValidation.Results;
using NzbDrone.Common;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
@ -32,10 +34,17 @@ namespace NzbDrone.Core.Indexers.Newznab
return ApiKeyWhiteList.Any(c => settings.Url.ToLowerInvariant().Contains(c));
}
private static readonly Regex AdditionalParametersRegex = new Regex(@"(&.+?\=.+?)+", RegexOptions.Compiled);
public NewznabSettingsValidator()
{
RuleFor(c => c.Url).ValidRootUrl();
RuleFor(c => c.ApiKey).NotEmpty().When(ShouldHaveApiKey);
RuleFor(c => c.Categories).NotEmpty().When(c => !c.AnimeCategories.Any());
RuleFor(c => c.AnimeCategories).NotEmpty().When(c => !c.Categories.Any());
RuleFor(c => c.AdditionalParameters)
.Matches(AdditionalParametersRegex)
.When(c => !c.AdditionalParameters.IsNullOrWhiteSpace());
}
}
@ -46,6 +55,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public NewznabSettings()
{
Categories = new[] { 5030, 5040 };
AnimeCategories = Enumerable.Empty<Int32>();
}
[FieldDefinition(0, Label = "URL")]
@ -54,8 +64,15 @@ namespace NzbDrone.Core.Indexers.Newznab
[FieldDefinition(1, Label = "API Key")]
public String ApiKey { get; set; }
[FieldDefinition(2, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows", Advanced = true)]
public IEnumerable<Int32> Categories { get; set; }
[FieldDefinition(3, Label = "Anime Categories", HelpText = "Comma Separated list, leave blank to disable anime", Advanced = true)]
public IEnumerable<Int32> AnimeCategories { get; set; }
[FieldDefinition(4, Label = "Additional Parameters", HelpText = "Additional newznab parameters", Advanced = true)]
public String AdditionalParameters { get; set; }
public ValidationResult Validate()
{
return Validator.Validate(this);

@ -24,43 +24,52 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
}
}
public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber)
public override IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber)
{
var searchUrls = new List<string>();
foreach (var url in RecentFeed)
{
searchUrls.Add(String.Format("{0}&search={1}+S{2:00}E{3:00}", url, seriesTitle, seasonNumber, episodeNumber));
foreach (var title in titles)
{
searchUrls.Add(String.Format("{0}&search={1}+S{2:00}E{3:00}", url, title, seasonNumber, episodeNumber));
}
}
return searchUrls;
}
public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
public override IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
{
var searchUrls = new List<String>();
foreach (var url in RecentFeed)
{
searchUrls.Add(String.Format("{0}&search={1}+{2:yyyy MM dd}", url, seriesTitle, date));
foreach (var title in titles)
{
searchUrls.Add(String.Format("{0}&search={1}+{2:yyyy MM dd}", url, title, date));
}
}
return searchUrls;
}
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber)
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
{
// TODO: Implement
return new List<string>();
}
public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset)
public override IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset)
{
var searchUrls = new List<String>();
foreach (var url in RecentFeed)
{
searchUrls.Add(String.Format("{0}&search={1}+S{2:00}", url, seriesTitle, seasonNumber));
foreach (var title in titles)
{
searchUrls.Add(String.Format("{0}&search={1}+S{2:00}", url, title, seasonNumber));
}
}
return searchUrls;

@ -22,24 +22,24 @@ namespace NzbDrone.Core.Indexers.Wombles
get { yield return "http://newshost.co.za/rss/?sec=TV&fr=false"; }
}
public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber)
public override IEnumerable<string> GetEpisodeSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int episodeNumber)
{
return new List<string>();
}
public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset)
public override IEnumerable<string> GetSeasonSearchUrls(List<String> titles, int tvRageId, int seasonNumber, int offset)
{
return new List<string>();
}
public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
public override IEnumerable<string> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
{
return new List<string>();
}
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber)
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
{
return new List<string>();
return new string[0];
}
public override IEnumerable<string> GetSearchUrls(string query, int offset)

@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.Indexers
@ -89,5 +90,30 @@ namespace NzbDrone.Core.Indexers
return element != null ? element.Value : defaultValue;
}
public static T TryGetValue<T>(this XElement item, string elementName, T defaultValue)
{
var element = item.Element(elementName);
if (element == null)
{
return defaultValue;
}
if (element.Value.IsNullOrWhiteSpace())
{
return defaultValue;
}
try
{
return (T)Convert.ChangeType(element.Value, typeof(T));
}
catch (InvalidCastException)
{
return defaultValue;
}
}
}
}

@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.ThingiProvider;

@ -6,6 +6,6 @@ namespace NzbDrone.Core.MetadataSource
{
public interface IProvideSeriesInfo
{
Tuple<Series, List<Episode>> GetSeriesInfo(int tvDbSeriesId);
Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId);
}
}

@ -80,10 +80,10 @@ namespace NzbDrone.Core.MetadataSource
}
}
public Tuple<Series, List<Episode>> GetSeriesInfo(int tvDbSeriesId)
public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
{
var client = BuildClient("show", "summary");
var restRequest = new RestRequest(tvDbSeriesId.ToString() + "/extended");
var restRequest = new RestRequest(tvdbSeriesId.ToString() + "/extended");
var response = client.ExecuteAndValidate<Show>(restRequest);
var episodes = response.seasons.SelectMany(c => c.episodes).Select(MapEpisode).ToList();
@ -111,7 +111,7 @@ namespace NzbDrone.Core.MetadataSource
series.Runtime = show.runtime;
series.Network = show.network;
series.AirTime = show.air_time;
series.TitleSlug = show.url.ToLower().Replace("http://trakt.tv/show/", "");
series.TitleSlug = GetTitleSlug(show.url);
series.Status = GetSeriesStatus(show.status, show.ended);
series.Ratings = GetRatings(show.ratings);
series.Genres = show.genres;
@ -131,7 +131,6 @@ namespace NzbDrone.Core.MetadataSource
var episode = new Episode();
episode.Overview = traktEpisode.overview;
episode.SeasonNumber = traktEpisode.season;
episode.EpisodeNumber = traktEpisode.episode;
episode.EpisodeNumber = traktEpisode.number;
episode.Title = traktEpisode.title;
episode.AirDate = FromIsoToString(traktEpisode.first_aired_iso);
@ -273,5 +272,17 @@ namespace NzbDrone.Core.MetadataSource
return seasons;
}
private static String GetTitleSlug(String url)
{
var slug = url.ToLower().Replace("http://trakt.tv/show/", "");
if (slug.StartsWith("."))
{
slug = "dot" + slug.Substring(1);
}
return slug;
}
}
}

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Tv;
using RestSharp;
namespace NzbDrone.Core.MetadataSource.Tvdb
{
public interface ITvdbProxy
{
List<Episode> GetEpisodeInfo(int tvdbSeriesId);
}
public class TvdbProxy : ITvdbProxy
{
public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
{
var client = BuildClient("series");
var request = new RestRequest(tvdbSeriesId + "/all");
var response = client.Execute(request);
var xml = XDocument.Load(new StringReader(response.Content));
var episodes = xml.Descendants("Episode").Select(MapEpisode).ToList();
var series = MapSeries(xml.Element("Series"));
return new Tuple<Series, List<Episode>>(series, episodes);
}
public List<Episode> GetEpisodeInfo(int tvdbSeriesId)
{
return GetSeriesInfo(tvdbSeriesId).Item2;
}
private static IRestClient BuildClient(string resource)
{
return new RestClient(String.Format("http://thetvdb.com/data/{0}", resource));
}
private static Series MapSeries(XElement item)
{
//TODO: We should map all the data incase we want to actually use it
var series = new Series();
return series;
}
private static Episode MapEpisode(XElement item)
{
//TODO: We should map all the data incase we want to actually use it
var episode = new Episode();
episode.SeasonNumber = item.TryGetValue("SeasonNumber", 0);
episode.EpisodeNumber = item.TryGetValue("EpisodeNumber", 0);
episode.AbsoluteEpisodeNumber = item.TryGetValue("absolute_number", 0);
return episode;
}
}
}

@ -118,10 +118,12 @@
<Compile Include="Configuration\ResetApiKeyCommand.cs" />
<Compile Include="DataAugmentation\DailySeries\DailySeriesDataProxy.cs" />
<Compile Include="DataAugmentation\DailySeries\DailySeriesService.cs" />
<Compile Include="DataAugmentation\Scene\ISceneMappingProvider.cs" />
<Compile Include="DataAugmentation\Scene\SceneMapping.cs" />
<Compile Include="DataAugmentation\Scene\SceneMappingService.cs" />
<Compile Include="DataAugmentation\Scene\SceneMappingProxy.cs" />
<Compile Include="DataAugmentation\Scene\SceneMappingRepository.cs" />
<Compile Include="DataAugmentation\Scene\ServicesProvider.cs" />
<Compile Include="DataAugmentation\Scene\UpdateSceneMappingCommand.cs" />
<Compile Include="DataAugmentation\Xem\Model\XemResult.cs" />
<Compile Include="DataAugmentation\Xem\Model\XemSceneTvdbMapping.cs" />
@ -198,6 +200,7 @@
<Compile Include="Datastore\Migration\049_fix_dognzb_url.cs" />
<Compile Include="Datastore\Migration\050_add_hash_to_metadata_files.cs" />
<Compile Include="Datastore\Migration\051_download_client_import.cs" />
<Compile Include="Datastore\Migration\052_add_columns_for_anime.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" />
@ -305,6 +308,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\UpdateAnimeCategories.cs" />
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
@ -321,10 +325,11 @@
<Compile Include="IndexerSearch\SeasonSearchCommand.cs" />
<Compile Include="IndexerSearch\SeasonSearchService.cs" />
<Compile Include="Indexers\BasicTorrentRssParser.cs" />
<Compile Include="Indexers\Animezb\Animezb.cs" />
<Compile Include="Indexers\Animezb\AnimezbParser.cs" />
<Compile Include="Indexers\DownloadProtocol.cs" />
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
<Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" />
<Compile Include="Indexers\Eztv\Eztv.cs" />
<Compile Include="Indexers\Fanzub\Fanzub.cs" />
<Compile Include="Indexers\Fanzub\FanzubParser.cs" />
<Compile Include="Indexers\FetchAndParseRssService.cs" />
@ -372,6 +377,7 @@
<Compile Include="MetadataSource\Trakt\Actor.cs" />
<Compile Include="MetadataSource\Trakt\People.cs" />
<Compile Include="MetadataSource\Trakt\Ratings.cs" />
<Compile Include="MetadataSource\Tvdb\TvdbProxy.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
@ -433,6 +439,7 @@
<Compile Include="Notifications\Xbmc\Model\XbmcJsonResult.cs" />
<Compile Include="Notifications\Xbmc\Model\XbmcVersion.cs" />
<Compile Include="Organizer\BasicNamingConfig.cs" />
<Compile Include="Organizer\AbsoluteEpisodeFormat.cs" />
<Compile Include="Organizer\FilenameValidationService.cs" />
<Compile Include="Organizer\EpisodeFormat.cs" />
<Compile Include="Organizer\Exception.cs" />

@ -0,0 +1,12 @@
using System;
namespace NzbDrone.Core.Organizer
{
public class EpisodeFormat
{
public String Separator { get; set; }
public String EpisodePattern { get; set; }
public String EpisodeSeparator { get; set; }
public String SeasonEpisodePattern { get; set; }
}
}

@ -2,11 +2,9 @@
namespace NzbDrone.Core.Organizer
{
public class EpisodeFormat
public class AbsoluteEpisodeFormat
{
public String Separator { get; set; }
public String EpisodePattern { get; set; }
public String EpisodeSeparator { get; set; }
public String SeasonEpisodePattern { get; set; }
public String AbsoluteEpisodePattern { get; set; }
}
}

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Cache;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
@ -38,9 +39,15 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?<absolute>\{absolute(?:\:0+)?})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>e|x)(?<episode>{episode(?:\:0+)?}))(?<separator>.+?(?={))?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<absolute>{absolute(?:\:0+)?})(?<separator>.+?(?={))?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>\s|\.|-|_)Title\})",
@ -118,6 +125,11 @@ namespace NzbDrone.Core.Organizer
}
}
if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber > 0))
{
pattern = namingConfig.AnimeEpisodeFormat;
}
var episodeFormat = GetEpisodeFormat(pattern);
if (episodeFormat != null)
@ -154,6 +166,34 @@ namespace NzbDrone.Core.Organizer
tokenValues.Add("{Season Episode}", seasonEpisodePattern);
}
//TODO: Extract to another method
var absoluteEpisodeFormat = GetAbsoluteFormat(pattern);
if (absoluteEpisodeFormat != null)
{
if (series.SeriesType != SeriesTypes.Anime)
{
pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "");
}
else
{
pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "{Absolute Pattern}");
var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern;
foreach (var episode in sortedEpisodes.Skip(1))
{
absoluteEpisodePattern += absoluteEpisodeFormat.Separator +
absoluteEpisodeFormat.AbsoluteEpisodePattern;
episodeTitles.Add(episode.Title.TrimEnd(EpisodeTitleTrimCharaters));
}
absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, sortedEpisodes);
tokenValues.Add("{Absolute Pattern}", absoluteEpisodePattern);
}
}
tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles));
tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality));
@ -310,10 +350,25 @@ namespace NzbDrone.Core.Organizer
var episodeIndex = 0;
pattern = EpisodeRegex.Replace(pattern, match =>
{
var episode = episodes[episodeIndex].EpisodeNumber;
var episode = episodes[episodeIndex];
episodeIndex++;
return ReplaceNumberToken(match.Groups["episode"].Value, episode);
return ReplaceNumberToken(match.Groups["episode"].Value, episode.EpisodeNumber);
});
return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber);
}
private string ReplaceAbsoluteNumberTokens(string pattern, List<Episode> episodes)
{
var episodeIndex = 0;
pattern = AbsoluteEpisodeRegex.Replace(pattern, match =>
{
var episode = episodes[episodeIndex];
episodeIndex++;
//TODO: We need to handle this null check somewhere, I think earlier is better...
return ReplaceNumberToken(match.Groups["absolute"].Value, episode.AbsoluteEpisodeNumber.Value);
});
return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber);
@ -354,6 +409,22 @@ namespace NzbDrone.Core.Organizer
});
}
private AbsoluteEpisodeFormat GetAbsoluteFormat(string pattern)
{
var match = AbsoluteEpisodePatternRegex.Match(pattern);
if (match.Success)
{
return new AbsoluteEpisodeFormat
{
Separator = match.Groups["separator"].Value,
AbsoluteEpisodePattern = match.Groups["absolute"].Value
};
}
return null;
}
private string GetEpisodeTitle(List<string> episodeTitles)
{
if (episodeTitles.Count == 1)

@ -22,6 +22,12 @@ namespace NzbDrone.Core.Organizer
return ruleBuilder.SetValidator(new ValidDailyEpisodeFormatValidator());
}
public static IRuleBuilderOptions<T, string> ValidAnimeEpisodeFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new ValidAnimeEpisodeFormatValidator());
}
public static IRuleBuilderOptions<T, string> ValidSeriesFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
@ -56,4 +62,26 @@ namespace NzbDrone.Core.Organizer
return true;
}
}
public class ValidAnimeEpisodeFormatValidator : PropertyValidator
{
public ValidAnimeEpisodeFormatValidator()
: base("Must contain Absolute Episode number or Season and Episode")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var value = context.PropertyValue as String;
if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) &&
!FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value))
{
return false;
}
return true;
}
}
}

@ -11,6 +11,7 @@ namespace NzbDrone.Core.Organizer
SampleResult GetStandardSample(NamingConfig nameSpec);
SampleResult GetMultiEpisodeSample(NamingConfig nameSpec);
SampleResult GetDailySample(NamingConfig nameSpec);
SampleResult GetAnimeSample(NamingConfig nameSpec);
String GetSeriesFolderSample(NamingConfig nameSpec);
String GetSeasonFolderSample(NamingConfig nameSpec);
}
@ -20,6 +21,7 @@ namespace NzbDrone.Core.Organizer
private readonly IBuildFileNames _buildFileNames;
private static Series _standardSeries;
private static Series _dailySeries;
private static Series _animeSeries;
private static Episode _episode1;
private static Episode _episode2;
private static List<Episode> _singleEpisode;
@ -27,10 +29,12 @@ namespace NzbDrone.Core.Organizer
private static EpisodeFile _singleEpisodeFile;
private static EpisodeFile _multiEpisodeFile;
private static EpisodeFile _dailyEpisodeFile;
private static EpisodeFile _animeEpisodeFile;
public FilenameSampleService(IBuildFileNames buildFileNames)
{
_buildFileNames = buildFileNames;
_standardSeries = new Series
{
SeriesType = SeriesTypes.Standard,
@ -43,19 +47,27 @@ namespace NzbDrone.Core.Organizer
Title = "Series Title"
};
_animeSeries = new Series
{
SeriesType = SeriesTypes.Anime,
Title = "Series Title"
};
_episode1 = new Episode
{
SeasonNumber = 1,
EpisodeNumber = 1,
Title = "Episode Title (1)",
AirDate = "2013-10-30"
AirDate = "2013-10-30",
AbsoluteEpisodeNumber = 1
};
_episode2 = new Episode
{
SeasonNumber = 1,
EpisodeNumber = 2,
Title = "Episode Title (2)"
Title = "Episode Title (2)",
AbsoluteEpisodeNumber = 2
};
_singleEpisode = new List<Episode> { _episode1 };
@ -81,6 +93,13 @@ namespace NzbDrone.Core.Organizer
Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv",
ReleaseGroup = "RlsGrp"
};
_animeEpisodeFile = new EpisodeFile
{
Quality = new QualityModel(Quality.HDTV720p),
Path = @"C:\Test\Series.Title.001.HDTV.x264-EVOLVE.mkv",
ReleaseGroup = "RlsGrp"
};
}
public SampleResult GetStandardSample(NamingConfig nameSpec)
@ -122,6 +141,19 @@ namespace NzbDrone.Core.Organizer
return result;
}
public SampleResult GetAnimeSample(NamingConfig nameSpec)
{
var result = new SampleResult
{
Filename = BuildSample(_singleEpisode, _animeSeries, _animeEpisodeFile, nameSpec),
Series = _animeSeries,
Episodes = _singleEpisode,
EpisodeFile = _animeEpisodeFile
};
return result;
}
public string GetSeriesFolderSample(NamingConfig nameSpec)
{
return _buildFileNames.GetSeriesFolder(_standardSeries.Title, nameSpec);

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NzbDrone.Core.Parser.Model;
@ -10,6 +11,7 @@ namespace NzbDrone.Core.Organizer
{
ValidationFailure ValidateStandardFilename(SampleResult sampleResult);
ValidationFailure ValidateDailyFilename(SampleResult sampleResult);
ValidationFailure ValidateAnimeFilename(SampleResult sampleResult);
}
public class FilenameValidationService : IFilenameValidationService
@ -62,6 +64,34 @@ namespace NzbDrone.Core.Organizer
return null;
}
public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult)
{
var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE);
var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename);
if (parsedEpisodeInfo == null)
{
return validationFailure;
}
if (parsedEpisodeInfo.AbsoluteEpisodeNumbers.Any())
{
if (!parsedEpisodeInfo.AbsoluteEpisodeNumbers.First().Equals(sampleResult.Episodes.First().AbsoluteEpisodeNumber))
{
return validationFailure;
}
return null;
}
if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo))
{
return validationFailure;
}
return null;
}
private bool ValidateSeasonAndEpisodeNumbers(List<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo)
{
if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber ||

@ -14,6 +14,7 @@ namespace NzbDrone.Core.Organizer
MultiEpisodeStyle = 0,
StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}",
DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Title}",
AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}",
SeriesFolderFormat = "{Series Title}",
SeasonFolderFormat = "Season {season}"
};
@ -24,6 +25,7 @@ namespace NzbDrone.Core.Organizer
public int MultiEpisodeStyle { get; set; }
public string StandardEpisodeFormat { get; set; }
public string DailyEpisodeFormat { get; set; }
public string AnimeEpisodeFormat { get; set; }
public string SeriesFolderFormat { get; set; }
public string SeasonFolderFormat { get; set; }
}

@ -17,6 +17,7 @@ namespace NzbDrone.Core.Parser.Model
public Language Language { get; set; }
public bool FullSeason { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public ParsedEpisodeInfo()
{
@ -58,6 +59,10 @@ namespace NzbDrone.Core.Parser.Model
{
episodeString = string.Format("S{0:00}E{1}", SeasonNumber, String.Join("-", EpisodeNumbers.Select(c => c.ToString("00"))));
}
else if (AbsoluteEpisodeNumbers != null && AbsoluteEpisodeNumbers.Any())
{
episodeString = string.Format("{0}", String.Join("-", AbsoluteEpisodeNumbers.Select(c => c.ToString("000"))));
}
return string.Format("{0} - {1} {2}", SeriesTitle, episodeString, Quality);
}

@ -23,15 +23,15 @@ namespace NzbDrone.Core.Parser
// RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title Absolute Episode Number + Season+Episode
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))(?<title>.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)",
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))(?<title>.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,3}))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>\[.{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title Season+Episode + Absolute Episode Number
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+",
new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.))(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.)(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.|$)+)+.*?(?<hash>\[.{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:[ ._-]+(?<absoluteepisode>\d{2,}))+",
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>\d{2,}))+.*?(?<hash>\[[a-z0-9]{8}\])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Multi-Part episodes without a title (S01E05.S01E06)
@ -54,16 +54,16 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+)))*)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//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+|_|$)(?!\\)",
//Anime - Title Absolute Episode Number [SubGroup]
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[.{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number [SubGroup]
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\](?:\.|$)",
//Anime - Title Absolute Episode Number Hash
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[.{8}\])(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 103/113 naming
new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>[1-9][0-9]|[0][1-9])(?!\w|\d+))+",
new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?!\w|\d+))+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1
@ -83,7 +83,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Supports 1103/1113 naming
new Regex(@"^(?<title>.+?)?(?:\W(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//4-digit episode number
@ -95,8 +95,16 @@ 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?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number
new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+e(?<absoluteepisode>\d{2,3}))+",
//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+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number (e66)
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[.{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Absolute Episode Number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}))+.*?(?<hash>\[.{8}\])?(?:$|\.)",
RegexOptions.IgnoreCase | RegexOptions.Compiled)
};
@ -115,7 +123,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[xh][\W_]?264|DD\W?5\W1|\<|\>|\?|\*|\:|\|",
private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[xh][\W_]?264|DD\W?5\W1|\<|\>|\?|\*|\:|\||848x480|1280x720|1920x1080|8bit|10bit",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AirDateRegex = new Regex(@"^(.*?)(?<!\d)((?<airyear>\d{4})[_.-](?<airmonth>[0-1][0-9])[_.-](?<airday>[0-3][0-9])|(?<airmonth>[0-1][0-9])[_.-](?<airday>[0-3][0-9])[_.-](?<airyear>\d{4}))(?!\d)",
@ -211,8 +219,21 @@ namespace NzbDrone.Core.Parser
Logger.Debug("Quality parsed: {0}", result.Quality);
result.ReleaseGroup = ParseReleaseGroup(title);
var subGroup = GetSubGroup(match);
if (!subGroup.IsNullOrWhiteSpace())
{
result.ReleaseGroup = subGroup;
}
Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup);
result.ReleaseHash = GetReleaseHash(match);
if (!result.ReleaseHash.IsNullOrWhiteSpace())
{
Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash);
}
return result;
}
}
@ -279,9 +300,7 @@ namespace NzbDrone.Core.Parser
const string defaultReleaseGroup = "DRONE";
title = title.Trim();
title = RemoveFileExtension(title);
title = title.TrimEnd("-RP");
var matches = ReleaseGroupRegex.Matches(title);
@ -564,5 +583,36 @@ namespace NzbDrone.Core.Parser
return true;
}
private static string GetSubGroup(MatchCollection matchCollection)
{
var subGroup = matchCollection[0].Groups["subgroup"];
if (subGroup.Success)
{
return subGroup.Value;
}
return String.Empty;
}
private static string GetReleaseHash(MatchCollection matchCollection)
{
var hash = matchCollection[0].Groups["hash"];
if (hash.Success)
{
var hashValue = hash.Value.Trim('[',']');
if (hashValue.Equals("1280x720"))
{
return String.Empty;
}
return hashValue;
}
return String.Empty;
}
}
}

@ -151,18 +151,50 @@ namespace NzbDrone.Core.Parser
if (parsedEpisodeInfo.IsAbsoluteNumbering())
{
var sceneSeasonNumber = _sceneMappingService.GetSeasonNumber(parsedEpisodeInfo.SeriesTitle);
foreach (var absoluteEpisodeNumber in parsedEpisodeInfo.AbsoluteEpisodeNumbers)
{
var episodeInfo = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber);
Episode episode = null;
if (sceneSource)
{
if (sceneSeasonNumber.HasValue && sceneSeasonNumber > 1)
{
var episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
if (episodes.Count == 1)
{
episode = episodes.First();
}
if (episode == null)
{
episode = _episodeService.FindEpisode(series.Id, sceneSeasonNumber.Value,
absoluteEpisodeNumber);
}
}
else
{
episode = _episodeService.FindEpisodeBySceneNumbering(series.Id, absoluteEpisodeNumber);
}
}
if (episodeInfo != null)
if (episode == null)
{
episode = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber);
}
if (episode != null)
{
_logger.Info("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}",
absoluteEpisodeNumber,
series.Title,
episodeInfo.SeasonNumber,
episodeInfo.EpisodeNumber);
result.Add(episodeInfo);
episode.SeasonNumber,
episode.EpisodeNumber);
result.Add(episode);
}
}

@ -31,7 +31,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex ProperRegex = new Regex(@"\b(?<proper>proper|repack)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p)|(?<_576p>576p)|(?<_720p>720p)|(?<_1080p>1080p))\b",
private static readonly Regex ResolutionRegex = new Regex(@"\b(?:(?<_480p>480p|640x480)|(?<_576p>576p)|(?<_720p>720p)|(?<_1080p>1080p))\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx))\b",
@ -39,6 +39,8 @@ namespace NzbDrone.Core.Parser
private static readonly Regex OtherSourceRegex = new Regex(@"(?<hdtv>HD[-_. ]TV)|(?<sdtv>SD[-_. ]TV)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AnimeBlurayRegex = new Regex(@"bd(?:720|1080)|(?<=\[|\(|\s)bd(?=\s|\)|\])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static QualityModel ParseQuality(string name)
{
Logger.Debug("Trying to parse quality for {0}", name);
@ -165,6 +167,25 @@ namespace NzbDrone.Core.Parser
return result;
}
//Anime Bluray matching
if (AnimeBlurayRegex.Match(normalizedName).Success)
{
if (resolution == Resolution._480p || resolution == Resolution._576p || normalizedName.Contains("480p"))
{
result.Quality = Quality.DVD;
return result;
}
if (resolution == Resolution._1080p || normalizedName.Contains("1080p"))
{
result.Quality = Quality.Bluray1080p;
return result;
}
result.Quality = Quality.Bluray720p;
return result;
}
if (resolution == Resolution._1080p)
{
result.Quality = Quality.HDTV1080p;
@ -177,12 +198,48 @@ namespace NzbDrone.Core.Parser
return result;
}
if (resolution == Resolution._480p)
{
result.Quality = Quality.SDTV;
return result;
}
if (codecRegex.Groups["x264"].Success)
{
result.Quality = Quality.SDTV;
return result;
}
if (normalizedName.Contains("848x480"))
{
if (normalizedName.Contains("dvd"))
{
result.Quality = Quality.DVD;
}
result.Quality = Quality.SDTV;
}
if (normalizedName.Contains("1280x720"))
{
if (normalizedName.Contains("bluray"))
{
result.Quality = Quality.Bluray720p;
}
result.Quality = Quality.HDTV720p;
}
if (normalizedName.Contains("1920x1080"))
{
if (normalizedName.Contains("bluray"))
{
result.Quality = Quality.Bluray1080p;
}
result.Quality = Quality.HDTV1080p;
}
if (normalizedName.Contains("bluray720p"))
{
result.Quality = Quality.Bluray720p;

@ -26,6 +26,7 @@ namespace NzbDrone.Core.Tv
public string Overview { get; set; }
public Boolean Monitored { get; set; }
public Nullable<Int32> AbsoluteEpisodeNumber { get; set; }
public Nullable<Int32> SceneAbsoluteEpisodeNumber { get; set; }
public int SceneSeasonNumber { get; set; }
public int SceneEpisodeNumber { get; set; }
public Ratings Ratings { get; set; }

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Linq;
using Marr.Data.QGen;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Messaging.Events;
@ -25,6 +26,7 @@ namespace NzbDrone.Core.Tv
PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials);
PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate);
void SetMonitoredFlat(Episode episode, bool monitored);
void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored);
@ -137,6 +139,20 @@ namespace NzbDrone.Core.Tv
.AndWhere(s => s.SceneEpisodeNumber == episodeNumber);
}
public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
{
var episodes = Query.Where(s => s.SeriesId == seriesId)
.AndWhere(s => s.SceneAbsoluteEpisodeNumber == sceneAbsoluteEpisodeNumber)
.ToList();
if (episodes.Empty() || episodes.Count > 1)
{
return null;
}
return episodes.Single();
}
public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate)
{
return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)

@ -7,7 +7,6 @@ using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Tv
{
@ -18,6 +17,7 @@ namespace NzbDrone.Core.Tv
Episode FindEpisode(int seriesId, int absoluteEpisodeNumber);
Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
Episode GetEpisode(int seriesId, String date);
Episode FindEpisode(int seriesId, String date);
List<Episode> GetEpisodeBySeries(int seriesId);
@ -71,6 +71,11 @@ namespace NzbDrone.Core.Tv
return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, seasonNumber, episodeNumber);
}
public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
{
return _episodeRepository.FindEpisodeBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber);
}
public Episode GetEpisode(int seriesId, String date)
{
return _episodeRepository.Get(seriesId, date);

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms.VisualStyles;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource.Tvdb;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Tv
@ -17,12 +17,14 @@ namespace NzbDrone.Core.Tv
public class RefreshEpisodeService : IRefreshEpisodeService
{
private readonly IEpisodeService _episodeService;
private readonly ITvdbProxy _tvdbProxy;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public RefreshEpisodeService(IEpisodeService episodeService, IEventAggregator eventAggregator, Logger logger)
public RefreshEpisodeService(IEpisodeService episodeService, ITvdbProxy tvdbProxy, IEventAggregator eventAggregator, Logger logger)
{
_episodeService = episodeService;
_tvdbProxy = tvdbProxy;
_eventAggregator = eventAggregator;
_logger = logger;
}
@ -68,6 +70,13 @@ namespace NzbDrone.Core.Tv
episodeToUpdate.Ratings = episode.Ratings;
episodeToUpdate.Images = episode.Images;
//Reset the absolute episode number to zero if the series is not anime
if (series.SeriesType != SeriesTypes.Anime)
{
episodeToUpdate.AbsoluteEpisodeNumber = 0;
}
successCount++;
}
catch (Exception e)
@ -82,7 +91,7 @@ namespace NzbDrone.Core.Tv
allEpisodes.AddRange(updateList);
AdjustMultiEpisodeAirTime(series, allEpisodes);
SetAbsoluteEpisodeNumber(allEpisodes);
SetAbsoluteEpisodeNumber(series, allEpisodes);
_episodeService.DeleteMany(existingEpisodes);
_episodeService.UpdateMany(updateList);
@ -144,15 +153,30 @@ namespace NzbDrone.Core.Tv
}
}
private static void SetAbsoluteEpisodeNumber(IEnumerable<Episode> allEpisodes)
private void SetAbsoluteEpisodeNumber(Series series, IEnumerable<Episode> allEpisodes)
{
var episodes = allEpisodes.Where(e => e.SeasonNumber > 0 && e.EpisodeNumber > 0)
.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)
.ToList();
if (series.SeriesType != SeriesTypes.Anime)
{
_logger.Debug("Skipping absolute number lookup for non-anime");
return;
}
for (int i = 0; i < episodes.Count(); i++)
var tvdbEpisodes = _tvdbProxy.GetEpisodeInfo(series.TvdbId);
foreach (var episode in allEpisodes)
{
episodes[i].AbsoluteEpisodeNumber = i + 1;
//I'd use single, but then I'd have to trust the tvdb data... and I don't
var tvdbEpisode = tvdbEpisodes.FirstOrDefault(e => e.SeasonNumber == episode.SeasonNumber &&
e.EpisodeNumber == episode.EpisodeNumber);
if (tvdbEpisode == null)
{
_logger.Debug("Cannot find matching episode from the tvdb: {0}x{1:00}", episode.SeasonNumber, episode.EpisodeNumber);
continue;
}
episode.AbsoluteEpisodeNumber = tvdbEpisode.AbsoluteEpisodeNumber;
}
}
}

@ -54,6 +54,7 @@ namespace NzbDrone.Core.Tv
var seriesInfo = tuple.Item1;
series.Title = seriesInfo.Title;
series.TitleSlug = seriesInfo.TitleSlug;
series.AirTime = seriesInfo.AirTime;
series.Overview = seriesInfo.Overview;
series.Status = seriesInfo.Status;

@ -8,6 +8,7 @@ using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Tv
@ -77,7 +78,7 @@ namespace NzbDrone.Core.Tv
_logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path);
newSeries.Monitored = true;
newSeries.CleanTitle = Parser.Parser.CleanSeriesTitle(newSeries.Title);
newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle();
_seriesRepository.Insert(newSeries);
_eventAggregator.PublishEvent(new SeriesAddedEvent(GetSeries(newSeries.Id)));
@ -97,8 +98,6 @@ namespace NzbDrone.Core.Tv
public Series FindByTitle(string title)
{
title = Parser.Parser.CleanSeriesTitle(title);
var tvdbId = _sceneMappingService.GetTvDbId(title);
if (tvdbId.HasValue)
@ -106,13 +105,13 @@ namespace NzbDrone.Core.Tv
return FindByTvdbId(tvdbId.Value);
}
return _seriesRepository.FindByTitle(title);
return _seriesRepository.FindByTitle(title.CleanSeriesTitle());
}
public Series FindByTitleInexact(string title)
{
// find any series clean title within the provided release title
string cleanTitle = Parser.Parser.CleanSeriesTitle(title);
string cleanTitle = title.CleanSeriesTitle();
var list = _seriesRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList();
if (!list.Any())
{

@ -30,11 +30,13 @@ namespace NzbDrone.Integration.Test
config.RenameEpisodes = false;
config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}";
config.AnimeEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
var result = NamingConfig.Put(config);
result.RenameEpisodes.Should().BeFalse();
result.StandardEpisodeFormat.Should().Be(config.StandardEpisodeFormat);
result.DailyEpisodeFormat.Should().Be(config.DailyEpisodeFormat);
result.AnimeEpisodeFormat.Should().Be(config.AnimeEpisodeFormat);
}
[Test]
@ -44,6 +46,7 @@ namespace NzbDrone.Integration.Test
config.RenameEpisodes = true;
config.StandardEpisodeFormat = "";
config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}";
config.AnimeEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
var errors = NamingConfig.InvalidPut(config);
errors.Should().NotBeEmpty();
@ -56,6 +59,7 @@ namespace NzbDrone.Integration.Test
config.RenameEpisodes = true;
config.StandardEpisodeFormat = "{season}";
config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}";
config.AnimeEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
var errors = NamingConfig.InvalidPut(config);
errors.Should().NotBeEmpty();
@ -68,6 +72,20 @@ namespace NzbDrone.Integration.Test
config.RenameEpisodes = true;
config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
config.DailyEpisodeFormat = "{Series Title} - {season} - {Episode Title}";
config.AnimeEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
var errors = NamingConfig.InvalidPut(config);
errors.Should().NotBeEmpty();
}
[Test]
public void should_get_bad_request_if_anime_format_doesnt_contain_season_and_episode_or_absolute()
{
var config = NamingConfig.GetSingle();
config.RenameEpisodes = false;
config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}";
config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}";
config.AnimeEpisodeFormat = "{Series Title} - {season} - {Episode Title}";
var errors = NamingConfig.InvalidPut(config);
errors.Should().NotBeEmpty();

@ -1,4 +1,5 @@
using FluentAssertions;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Api.Indexers;

@ -21,7 +21,7 @@ namespace NzbDrone.Test.Common
LogManager.Configuration = new LoggingConfiguration();
var consoleTarget = new ConsoleTarget { Layout = "${level}: ${message} ${exception}" };
LogManager.Configuration.AddTarget(consoleTarget.GetType().Name, consoleTarget);
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, consoleTarget));
LogManager.Configuration.LoggingRules.Add(new LoggingRule("*", LogLevel.Trace, consoleTarget));
RegisterExceptionVerification();
}

@ -458,8 +458,7 @@ Global
{911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|Any CPU.ActiveCfg = Release|Any CPU
@ -473,8 +472,8 @@ Global
{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.Build.0 = Debug|x86
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86
{40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|Any CPU.ActiveCfg = Release|Any CPU

@ -32,12 +32,13 @@ define(
template: 'AddSeries/SearchResultViewTemplate',
ui: {
qualityProfile: '.x-quality-profile',
rootFolder : '.x-root-folder',
seasonFolder : '.x-season-folder',
addButton : '.x-add',
overview : '.x-overview',
startingSeason: '.x-starting-season'
qualityProfile : '.x-quality-profile',
rootFolder : '.x-root-folder',
seasonFolder : '.x-season-folder',
seriesType : '.x-series-type',
startingSeason : '.x-starting-season',
addButton : '.x-add',
overview : '.x-overview'
},
events: {
@ -151,12 +152,14 @@ define(
var quality = this.ui.qualityProfile.val();
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var startingSeason = this.ui.startingSeason.val();
var seriesType = this.ui.seriesType.val();
var seasonFolder = this.ui.seasonFolder.prop('checked');
this.model.set('qualityProfileId', quality);
this.model.set('rootFolderPath', rootFolderPath);
this.model.setSeasonPass(startingSeason);
this.model.set('seasonFolder', seasonFolder);
this.model.set('seriesType', seriesType);
this.model.setSeasonPass(startingSeason);
var self = this;
var promise = this.model.save();

@ -33,14 +33,22 @@
{{> RootFolderSelectionPartial rootFolders}}
</div>
{{/unless}}
<div class="form-group col-md-2">
<label>Starting Season</label>
{{> StartingSeasonSelectionPartial seasons}}
</div>
<div class="form-group col-md-2">
<label>Quality Profile</label>
{{> QualityProfileSelectionPartial qualityProfiles}}
</div>
<div class="form-group col-md-2">
<label>Series Type</label>
{{> SeriesTypeSelectionPartial}}
</div>
<div class="form-group col-md-2">
<label>Season Folders</label>
@ -55,20 +63,23 @@
</label>
</div>
</div>
<div class="form-group col-md-1 pull-right">
<label>&nbsp;</label>
<button class="btn btn-success x-add add-series pull-right pull-none-xs"> Add
{{/unless}}
</div>
<div class="row">
{{#unless existing}}
<div class="form-group col-md-2 col-md-offset-10">
<!--Uncomment if we need to add even more controls to add series-->
<!--<label>&nbsp;</label>-->
<button class="btn btn-success x-add"> Add
<i class="icon-plus"></i>
</button>
</div>
{{else}}
<div class="col-md-1 col-md-offset-11">
<button class="btn add-series disabled pull-right pull-none-xs">
<div class="col-md-2 col-md-offset-10">
<button class="btn add-series disabled">
Already Exists
</button>
</div>
{{/unless}}
</div>
</div>

@ -0,0 +1,5 @@
<select class="form-control col-md-2 x-series-type" name="seriesType">
<option value="standard">Standard</option>
<option value="daily">Daily</option>
<option value="anime">Anime</option>
</select>

@ -1,4 +1,4 @@
<select class="form-control md-col-2 starting-season x-starting-season">
<select class="form-control col-md-2 starting-season x-starting-season">
{{#each this}}
{{#if_eq seasonNumber compare="0"}}
<option value="{{seasonNumber}}">Specials</option>

@ -85,21 +85,9 @@
font-size : 16px;
}
.add-series {
margin-left : 20px;
}
.checkbox {
margin-top : 0px;
}
.starting-season {
width: 140px;
&.starting-season-label {
display: inline-block;
margin-top : 0px;
}
}
i {
&:before {

@ -11,7 +11,6 @@ define(
className: 'approval-status-cell',
template : 'Cells/ApprovalStatusCellTemplate',
render: function () {
var rejections = this.model.get(this.column.get('name'));

@ -164,3 +164,7 @@ td.delete-episode-file-cell {
.series-status-cell {
width: 16px;
}
.episode-number-cell {
cursor : default;
}

@ -1,4 +1,4 @@
<div class="form-group">
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">

@ -1,4 +1,4 @@
<div class="form-group">
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">

@ -1,4 +1,4 @@
<div class="form-group">
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">

@ -1,4 +1,4 @@
<div class="form-group">
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">

@ -1,4 +1,4 @@
<div class="form-group">
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">

@ -0,0 +1,62 @@
'use strict';
define(
[
'marionette',
'Cells/NzbDroneCell',
'reqres'
], function (Marionette, NzbDroneCell, reqres) {
return NzbDroneCell.extend({
className: 'episode-number-cell',
template : 'Series/Details/EpisodeNumberCellTemplate',
render: function () {
this.$el.empty();
this.$el.html(this.model.get('episodeNumber'));
var alternateTitles = [];
if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) {
if (this.model.get('sceneSeasonNumber') > 0) {
alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber,
this.model.get('seriesId'),
this.model.get('sceneSeasonNumber'));
}
if (alternateTitles.length === 0) {
alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber,
this.model.get('seriesId'),
this.model.get('seasonNumber'));
}
}
if (this.model.get('sceneSeasonNumber') > 0 ||
this.model.get('sceneEpisodeNumber') > 0 ||
(this.model.has('sceneAbsoluteEpisodeNumber') && this.model.get('sceneAbsoluteEpisodeNumber') > 0) ||
alternateTitles.length > 0)
{
this.templateFunction = Marionette.TemplateCache.get(this.template);
var json = this.model.toJSON();
json.alternateTitles = alternateTitles;
var html = this.templateFunction(json);
this.$el.popover({
content : html,
html : true,
trigger : 'hover',
title : 'Scene Information',
placement: 'right',
container: this.$el
});
}
this.delegateEvents();
return this;
}
});
});

@ -0,0 +1,39 @@
<div class="scene-info">
{{#if sceneSeasonNumber}}
<div class="row">
<div class="key">Season</div>
<div class="value">{{sceneSeasonNumber}}</div>
</div>
{{/if}}
{{#if sceneEpisodeNumber}}
<div class="row">
<div class="key">Episode</div>
<div class="value">{{sceneEpisodeNumber}}</div>
</div>
{{/if}}
{{#if sceneAbsoluteEpisodeNumber}}
<div class="row">
<div class="key">Absolute</div>
<div class="value">{{sceneAbsoluteEpisodeNumber}}</div>
</div>
{{/if}}
{{#if alternateTitles}}
<div class="row">
{{#if_gt alternateTitles.length compare="1"}}
<div class="key">Titles</div>
{{else}}
<div class="key">Title</div>
{{/if_gt}}
<div class="value">
<ul>
{{#each alternateTitles}}
<li>{{title}}</li>
{{/each}}
</ul>
</div>
</div>
{{/if}}
</div>

@ -30,8 +30,10 @@
</div>
<div class="row">
<div class="col-md-12">
{{#each alternativeTitles}}
<span class="label label-default">{{this}}</span>
{{#each alternateTitles}}
{{#if_eq seasonNumber compare="-1"}}
<span class="label label-default">{{title}}</span>
{{/if_eq}}
{{/each}}
</div>
</div>

@ -9,6 +9,7 @@ define(
'Cells/RelativeDateCell',
'Cells/EpisodeStatusCell',
'Cells/EpisodeActionsCell',
'Series/Details/EpisodeNumberCell',
'Commands/CommandController',
'moment',
'underscore',
@ -21,6 +22,7 @@ define(
RelativeDateCell,
EpisodeStatusCell,
EpisodeActionsCell,
EpisodeNumberCell,
CommandController,
Moment,
_,
@ -58,11 +60,9 @@ define(
sortable : false
},
{
name : 'episodeNumber',
name : 'this',
label: '#',
cell : Backgrid.IntegerCell.extend({
className: 'episode-number-cell'
})
cell : EpisodeNumberCell
},
{
name : 'this',

@ -191,6 +191,14 @@ define(
return self.episodeFileCollection.get(episodeFileId);
});
reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function (seriesId, seasonNumber) {
if (self.model.get('id') !== seriesId) {
return [];
}
return _.where(self.model.get('alternateTitles'), { seasonNumber: seasonNumber });
});
$.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function () {
var seasonCollectionView = new SeasonCollectionView({
collection : self.seasonCollection,

@ -58,10 +58,10 @@
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="inputQualityProfile">Quality Profile</label>
<label class="col-sm-4 control-label">Quality Profile</label>
<div class="col-sm-4">
<select class="form-control x-quality-profile" id="inputQualityProfile" name="qualityProfileId">
<select class="form-control x-quality-profile" name="qualityProfileId">
{{#each qualityProfiles.models}}
<option value="{{id}}">{{attributes.name}}</option>
{{/each}}
@ -71,10 +71,17 @@
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="inputPath">Path</label>
<label class="col-sm-4 control-label">Series Type</label>
<div class="col-sm-4">
{{> SeriesTypeSelectionPartial}}
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Path</label>
<div class="col-sm-6">
<input type="text" id="inputPath" class="form-control x-path" placeholder="Path" name="path">
<input type="text" class="form-control x-path" placeholder="Path" name="path">
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save