mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-04 10:02:40 +01:00
Anime!
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
This commit is contained in:
parent
828dd5f5ad
commit
193672b652
@ -51,7 +51,7 @@ public void matching_fields(Type modelType, Type resourceType)
|
||||
}
|
||||
|
||||
[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 @@ public static List<Field> ToSchema(object model)
|
||||
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 @@ public static object ReadFormSchema(List<Field> fields, Type targetType, object
|
||||
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 @@ public NamingConfigModule(INamingConfigService namingConfigService,
|
||||
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 @@ private JsonResponse<NamingSampleResource> GetExamples(NamingConfigResource conf
|
||||
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 @@ private JsonResponse<NamingSampleResource> GetExamples(NamingConfigResource conf
|
||||
? "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 @@ public class NamingConfigResource : RestResource
|
||||
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 class NamingSampleResource
|
||||
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 @@ public class EpisodeResource : RestResource
|
||||
|
||||
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 @@ public class ReleaseModule : NzbDroneRestModule<ReleaseResource>
|
||||
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 @@ private List<ReleaseResource> GetReleases()
|
||||
|
||||
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 @@ public class ReleaseResource : RestResource
|
||||
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 @@ public class ReleaseResource : RestResource
|
||||
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" />
|
||||
|
10
src/NzbDrone.Api/Series/AlternateTitleResource.cs
Normal file
10
src/NzbDrone.Api/Series/AlternateTitleResource.cs
Normal file
@ -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.Core.Tv.Events;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using NzbDrone.Core.DataAugmentation.Scene;
|
||||
using Omu.ValueInjecter;
|
||||
|
||||
namespace NzbDrone.Api.Series
|
||||
{
|
||||
@ -78,21 +79,6 @@ SeriesAncestorValidator seriesAncestorValidator
|
||||
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 @@ private SeriesResource GetSeriesResource(Core.Tv.Series series)
|
||||
var resource = series.InjectTo<SeriesResource>();
|
||||
MapCoversToLocal(resource);
|
||||
FetchAndLinkSeriesStatistics(resource);
|
||||
PopulateAlternativeTitles(resource);
|
||||
PopulateAlternateTitles(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
@ -118,7 +104,7 @@ private List<SeriesResource> AllSeries()
|
||||
|
||||
MapCoversToLocal(seriesResources.ToArray());
|
||||
LinkSeriesStatistics(seriesResources, seriesStats);
|
||||
PopulateAlternativeTitles(seriesResources);
|
||||
PopulateAlternateTitles(seriesResources);
|
||||
|
||||
return seriesResources;
|
||||
}
|
||||
@ -179,6 +165,23 @@ private void LinkSeriesStatistics(SeriesResource resource, SeriesStatistics seri
|
||||
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 @@ public class SeriesResource : RestResource
|
||||
|
||||
//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 @@ public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item
|
||||
|
||||
source.Add(item);
|
||||
}
|
||||
|
||||
public static bool Empty<TSource>(this IEnumerable<TSource> source)
|
||||
{
|
||||
return !source.Any();
|
||||
}
|
||||
}
|
||||
}
|
@ -15,16 +15,16 @@ public static List<PropertyInfo> GetSimpleProperties(this Type type)
|
||||
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 @@ public void Setup()
|
||||
_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 @@ public void UpdateMappings_purge_existing_mapping_and_add_new_ones()
|
||||
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 @@ public void UpdateMappings_should_not_delete_if_fetch_returns_empty_list()
|
||||
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 @@ public void SetUp()
|
||||
.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 @@ public void SetUp()
|
||||
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 @@ private void WithEpisodes()
|
||||
|
||||
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 @@ private List<SearchCriteriaBase> WatchForSearchCriteria()
|
||||
.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 @@ public void scene_seasonsearch_should_use_seasonnumber_if_no_scene_number_is_ava
|
||||
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 @@ public class SearchDefinitionFixture : CoreTest<SingleEpisodeSearchCriteria>
|
||||
[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 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 @@ private IndexerBase<TestIndexerSettings> WithIndexer(bool paging, int resultCoun
|
||||
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 @@ private IndexerBase<TestIndexerSettings> WithIndexer(bool paging, int resultCoun
|
||||
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 @@ public void should_not_use_offset_if_result_count_is_less_than_90()
|
||||
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 @@ public void should_not_use_offset_for_sites_that_do_not_support_it()
|
||||
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 @@ public void getting_details_of_invalid_series()
|
||||
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();
|
||||
|
120
src/NzbDrone.Core.Test/MetadataSourceTests/TvdbProxyFixture.cs
Normal file
120
src/NzbDrone.Core.Test/MetadataSourceTests/TvdbProxyFixture.cs
Normal file
@ -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 @@ public void Setup()
|
||||
.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 @@ public void should_replace_triple_period_with_single_period()
|
||||
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 @@ public class AbsoluteEpisodeNumberParserFixture : CoreTest
|
||||
[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 @@ public class QualityParserFixture : CoreTest
|
||||
[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 @@ public void should_parse_sdtv_quality(string title, bool proper)
|
||||
[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 @@ public void should_parse_webdl480p_quality(string title, bool proper)
|
||||
[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 @@ public void should_parse_hdtv720p_quality(string title, bool proper)
|
||||
[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 @@ public void should_parse_webdl1080p_quality(string title, bool proper)
|
||||
[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 @@ public void should_parse_bluray720p_quality(string title, bool proper)
|
||||
[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 @@ public void should_remove_duplicate_remote_episodes_before_processing()
|
||||
}
|
||||
|
||||
[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 @@ public FieldDefinitionAttribute(int order)
|
||||
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 @@ public class SceneMapping : ModelBase
|
||||
|
||||
[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 @@ public List<SceneMapping> FindByTvdbid(int tvdbId)
|
||||
{
|
||||
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 @@ public Nullable<Int32> GetTvDbId(string cleanName)
|
||||
|
||||
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.DistinctBy(s => s.ParseTerm).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Received empty list of mapping. will not update.");
|
||||
}
|
||||
|
||||
_repository.InsertMany(mappings);
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
20
src/NzbDrone.Core/DataAugmentation/Scene/ServicesProvider.cs
Normal file
20
src/NzbDrone.Core/DataAugmentation/Scene/ServicesProvider.cs
Normal file
@ -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 @@ public interface IXemProxy
|
||||
{
|
||||
List<int> GetXemSeriesIds();
|
||||
List<XemSceneTvdbMapping> GetSceneTvdbMappings(int id);
|
||||
List<SceneMapping> GetSceneTvdbNames();
|
||||
}
|
||||
|
||||
public class XemProxy : IXemProxy
|
||||
@ -65,6 +68,47 @@ public List<XemSceneTvdbMapping> GetSceneTvdbMappings(int id)
|
||||
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 @@ private static void CheckForFailureResult<T>(XemResult<T> response)
|
||||
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 @@ private void PerformUpdate(Series series)
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
episode.AbsoluteEpisodeNumber = 0;
|
||||
episode.SceneAbsoluteEpisodeNumber = 0;
|
||||
episode.SceneSeasonNumber = 0;
|
||||
episode.SceneEpisodeNumber = 0;
|
||||
}
|
||||
@ -64,7 +66,7 @@ private void PerformUpdate(Series series)
|
||||
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 @@ private void RefreshCache()
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public void Handle(EpisodeGrabbedEvent message)
|
||||
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 @@ public class AnimeEpisodeSearchCriteria : SearchCriteriaBase
|
||||
|
||||
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 @@ public class DailyEpisodeSearchCriteria : SearchCriteriaBase
|
||||
|
||||
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 @@ public abstract class SearchCriteriaBase
|
||||
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 @@ public class SeasonSearchCriteria : SearchCriteriaBase
|
||||
|
||||
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 @@ public class SingleEpisodeSearchCriteria : SearchCriteriaBase
|
||||
|
||||
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 @@ public class SpecialEpisodeSearchCriteria : SearchCriteriaBase
|
||||
|
||||
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 @@ public List<DownloadDecision> EpisodeSearch(Episode episode)
|
||||
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 @@ private List<DownloadDecision> SearchDaily(Series series, Episode episode)
|
||||
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 @@ private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episod
|
||||
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);
|
||||
var downloadDecisions = new List<DownloadDecision>();
|
||||
|
||||
if (seasonNumber == 0)
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
// 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 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);
|
||||
downloadDecisions.AddRange(SearchAnime(series, episode));
|
||||
}
|
||||
|
||||
return downloadDecisions;
|
||||
@ -207,13 +233,14 @@ public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber)
|
||||
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;
|
||||
}
|
||||
|
97
src/NzbDrone.Core/Indexers/Animezb/Animezb.cs
Normal file
97
src/NzbDrone.Core/Indexers/Animezb/Animezb.cs
Normal file
@ -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, "+");
|
||||
}
|
||||
}
|
||||
}
|
31
src/NzbDrone.Core/Indexers/Animezb/AnimezbParser.cs
Normal file
31
src/NzbDrone.Core/Indexers/Animezb/AnimezbParser.cs
Normal file
@ -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 @@ public override DownloadProtocol Protocol
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SupportsPaging
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SupportsSearching
|
||||
{
|
||||
get
|
||||
@ -43,33 +38,47 @@ public override IEnumerable<string> RecentFeed
|
||||
{
|
||||
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 @@ public interface IIndexer : IProvider
|
||||
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 @@ protected TSettings Settings
|
||||
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 @@ private IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCr
|
||||
{
|
||||
_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 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria se
|
||||
{
|
||||
_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 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria sea
|
||||
{
|
||||
_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 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, AnimeEpisodeSearchCriteria sea
|
||||
{
|
||||
_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 @@ public override IEnumerable<string> RecentFeed
|
||||
{
|
||||
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,14 +94,71 @@ public override IEnumerable<string> RecentFeed
|
||||
}
|
||||
}
|
||||
|
||||
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> GetDailyEpisodeSearchUrls(List<String> titles, int tvRageId, DateTime date)
|
||||
{
|
||||
if (Settings.Categories.Empty())
|
||||
{
|
||||
return Enumerable.Empty<String>();
|
||||
}
|
||||
|
||||
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 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(List<String> titles, int tvRageId, int absoluteEpisodeNumber)
|
||||
{
|
||||
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(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 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)
|
||||
@ -114,33 +169,6 @@ public override IEnumerable<string> GetSearchUrls(string query, int offset)
|
||||
return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query));
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetAnimeEpisodeSearchUrls(string seriesTitle, int tvRageId, int absoluteEpisodeNumber)
|
||||
{
|
||||
// TODO: Implement
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
private static string NewsnabifyTitle(string title)
|
||||
{
|
||||
return title.Replace("+", "%20");
|
||||
|
@ -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 @@ private static bool ShouldHaveApiKey(NewznabSettings settings)
|
||||
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 @@ public class NewznabSettings : IProviderConfig
|
||||
public NewznabSettings()
|
||||
{
|
||||
Categories = new[] { 5030, 5040 };
|
||||
AnimeCategories = Enumerable.Empty<Int32>();
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "URL")]
|
||||
@ -54,8 +64,15 @@ public NewznabSettings()
|
||||
[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 @@ public override IEnumerable<string> RecentFeed
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public override IEnumerable<string> RecentFeed
|
||||
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.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers
|
||||
@ -89,5 +90,30 @@ public static string TryGetValue(this XElement item, string elementName, string
|
||||
|
||||
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 @@ public List<Series> SearchForNewSeries(string title)
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ private static Series MapSeries(Show show)
|
||||
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 @@ private static Episode MapEpisode(Trakt.Episode traktEpisode)
|
||||
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 @@ private static Tv.Ratings GetRatings(Trakt.Ratings ratings)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
63
src/NzbDrone.Core/MetadataSource/Tvdb/TvdbProxy.cs
Normal file
63
src/NzbDrone.Core/MetadataSource/Tvdb/TvdbProxy.cs
Normal file
@ -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" />
|
||||
|
12
src/NzbDrone.Core/Organizer/AbsoluteEpisodeFormat.cs
Normal file
12
src/NzbDrone.Core/Organizer/AbsoluteEpisodeFormat.cs
Normal file
@ -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.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 @@ public class FileNameBuilder : IBuildFileNames
|
||||
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 @@ public string BuildFilename(IList<Episode> episodes, Series series, EpisodeFile
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public string BuildFilename(IList<Episode> episodes, Series series, EpisodeFile
|
||||
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 @@ private string ReplaceNumberTokens(string pattern, List<Episode> episodes)
|
||||
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 @@ private EpisodeFormat GetEpisodeFormat(string pattern)
|
||||
});
|
||||
}
|
||||
|
||||
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 @@ public static IRuleBuilderOptions<T, string> ValidDailyEpisodeFormat<T>(this IRu
|
||||
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 @@ protected override bool IsValid(PropertyValidatorContext context)
|
||||
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 @@ public interface IFilenameSampleService
|
||||
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 @@ public class FilenameSampleService : IFilenameSampleService
|
||||
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 @@ public class FilenameSampleService : IFilenameSampleService
|
||||
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 @@ public FilenameSampleService(IBuildFileNames buildFileNames)
|
||||
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 @@ public FilenameSampleService(IBuildFileNames buildFileNames)
|
||||
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 @@ public SampleResult GetDailySample(NamingConfig nameSpec)
|
||||
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 @@ public interface IFilenameValidationService
|
||||
{
|
||||
ValidationFailure ValidateStandardFilename(SampleResult sampleResult);
|
||||
ValidationFailure ValidateDailyFilename(SampleResult sampleResult);
|
||||
ValidationFailure ValidateAnimeFilename(SampleResult sampleResult);
|
||||
}
|
||||
|
||||
public class FilenameValidationService : IFilenameValidationService
|
||||
@ -62,6 +64,34 @@ public ValidationFailure ValidateDailyFilename(SampleResult sampleResult)
|
||||
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 @@ public static NamingConfig Default
|
||||
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 @@ public static NamingConfig Default
|
||||
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 @@ public class ParsedEpisodeInfo
|
||||
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 @@ public override string ToString()
|
||||
{
|
||||
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 @@ public static class 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 @@ public static class 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 @@ public static class 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 @@ public static class 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 @@ public static class 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 @@ public static ParsedEpisodeInfo ParseTitle(string title)
|
||||
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 @@ public static string ParseReleaseGroup(string title)
|
||||
const string defaultReleaseGroup = "DRONE";
|
||||
|
||||
title = title.Trim();
|
||||
|
||||
title = RemoveFileExtension(title);
|
||||
|
||||
title = title.TrimEnd("-RP");
|
||||
|
||||
var matches = ReleaseGroupRegex.Matches(title);
|
||||
@ -564,5 +583,36 @@ private static bool ValidateBeforeParsing(string title)
|
||||
|
||||
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 @@ public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser
|
||||
|
||||
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 (episodeInfo != 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 (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 @@ public class QualityParser
|
||||
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 @@ public class QualityParser
|
||||
|
||||
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 @@ public static QualityModel ParseQuality(string name)
|
||||
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 @@ public static QualityModel ParseQuality(string name)
|
||||
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 @@ public Episode()
|
||||
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.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 @@ public interface IEpisodeRepository : IBasicRepository<Episode>
|
||||
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 @@ public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber
|
||||
.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.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
@ -18,6 +17,7 @@ public interface IEpisodeService
|
||||
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 @@ public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber
|
||||
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 @@ public interface IRefreshEpisodeService
|
||||
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 @@ public void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisode
|
||||
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 @@ public void RefreshEpisodeInfo(Series series, IEnumerable<Episode> remoteEpisode
|
||||
allEpisodes.AddRange(updateList);
|
||||
|
||||
AdjustMultiEpisodeAirTime(series, allEpisodes);
|
||||
SetAbsoluteEpisodeNumber(allEpisodes);
|
||||
SetAbsoluteEpisodeNumber(series, allEpisodes);
|
||||
|
||||
_episodeService.DeleteMany(existingEpisodes);
|
||||
_episodeService.UpdateMany(updateList);
|
||||
@ -144,15 +153,30 @@ private static void AdjustMultiEpisodeAirTime(Series series, IEnumerable<Episode
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
for (int i = 0; i < episodes.Count(); i++)
|
||||
if (series.SeriesType != SeriesTypes.Anime)
|
||||
{
|
||||
episodes[i].AbsoluteEpisodeNumber = i + 1;
|
||||
_logger.Debug("Skipping absolute number lookup for non-anime");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var tvdbEpisodes = _tvdbProxy.GetEpisodeInfo(series.TvdbId);
|
||||
|
||||
foreach (var episode in allEpisodes)
|
||||
{
|
||||
//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 @@ private void RefreshSeriesInfo(Series series)
|
||||
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.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 @@ public Series AddSeries(Series newSeries)
|
||||
_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 @@ public Series FindByTvRageId(int tvRageId)
|
||||
|
||||
public Series FindByTitle(string title)
|
||||
{
|
||||
title = Parser.Parser.CleanSeriesTitle(title);
|
||||
|
||||
var tvdbId = _sceneMappingService.GetTvDbId(title);
|
||||
|
||||
if (tvdbId.HasValue)
|
||||
@ -106,13 +105,13 @@ public Series FindByTitle(string title)
|
||||
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 @@ public void should_be_able_to_update()
|
||||
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 @@ public void should_get_bad_request_if_standard_format_is_empty()
|
||||
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 @@ public void should_get_bad_request_if_standard_format_doesnt_contain_season_and_
|
||||
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 @@ public void should_get_bad_request_if_daily_format_doesnt_contain_season_and_epi
|
||||
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 @@ protected static void InitLogging()
|
||||
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> </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> </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>
|
||||
|
5
src/UI/AddSeries/SeriesTypeSelectionPartial.html
Normal file
5
src/UI/AddSeries/SeriesTypeSelectionPartial.html
Normal file
@ -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">
|
||||
|
62
src/UI/Series/Details/EpisodeNumberCell.js
Normal file
62
src/UI/Series/Details/EpisodeNumberCell.js
Normal file
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
39
src/UI/Series/Details/EpisodeNumberCellTemplate.html
Normal file
39
src/UI/Series/Details/EpisodeNumberCellTemplate.html
Normal file
@ -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…
Reference in New Issue
Block a user