Merge branch 'develop'
This commit is contained in:
commit
dcb226dde2
|
@ -3,4 +3,5 @@
|
||||||
<dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.dylib"/>
|
<dllmap os="osx" dll="MediaInfo.dll" target="libmediainfo.dylib"/>
|
||||||
<dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
<dllmap os="linux" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
||||||
<dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
<dllmap os="freebsd" dll="MediaInfo.dll" target="libmediainfo.so.0" />
|
||||||
|
<dllmap os="solaris" dll="MediaInfo.dll" target="libmediainfo.so.0.0.0" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -95,6 +95,9 @@
|
||||||
</None>
|
</None>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
using Nancy;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using DDay.iCal;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using Nancy.Responses;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Calendar
|
||||||
|
{
|
||||||
|
public class CalendarFeedModule : NzbDroneFeedModule
|
||||||
|
{
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
|
|
||||||
|
public CalendarFeedModule(IEpisodeService episodeService)
|
||||||
|
: base("calendar")
|
||||||
|
{
|
||||||
|
_episodeService = episodeService;
|
||||||
|
|
||||||
|
Get["/NzbDrone.ics"] = options => GetCalendarFeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response GetCalendarFeed()
|
||||||
|
{
|
||||||
|
var start = DateTime.Today.AddDays(-7);
|
||||||
|
var end = DateTime.Today.AddDays(28);
|
||||||
|
|
||||||
|
var queryStart = Request.Query.Start;
|
||||||
|
var queryEnd = Request.Query.End;
|
||||||
|
|
||||||
|
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
|
||||||
|
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
|
||||||
|
|
||||||
|
var episodes = _episodeService.EpisodesBetweenDates(start, end);
|
||||||
|
var icalCalendar = new iCalendar();
|
||||||
|
|
||||||
|
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
|
||||||
|
{
|
||||||
|
var occurrence = icalCalendar.Create<Event>();
|
||||||
|
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
|
||||||
|
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;
|
||||||
|
occurrence.Start = new iCalDateTime(episode.AirDateUtc.Value);
|
||||||
|
occurrence.End = new iCalDateTime(episode.AirDateUtc.Value.AddMinutes(episode.Series.Runtime));
|
||||||
|
occurrence.Description = episode.Overview;
|
||||||
|
occurrence.Categories = new List<string>() { episode.Series.Network };
|
||||||
|
|
||||||
|
switch (episode.Series.SeriesType)
|
||||||
|
{
|
||||||
|
case SeriesTypes.Daily:
|
||||||
|
occurrence.Summary = string.Format("{0} - {1}", episode.Series.Title, episode.Title);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
occurrence.Summary = string.Format("{0} - {1}x{2:00} - {3}", episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var serializer = new DDay.iCal.Serialization.iCalendar.SerializerFactory().Build(icalCalendar.GetType(), new DDay.iCal.Serialization.SerializationContext()) as DDay.iCal.Serialization.IStringSerializer;
|
||||||
|
var icalendar = serializer.SerializeToString(icalCalendar);
|
||||||
|
|
||||||
|
return new TextResponse(icalendar, "text/calendar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ namespace NzbDrone.Api.Frontend.Mappers
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
return resourceUrl.StartsWith("/log/") && resourceUrl.EndsWith(".txt");
|
return resourceUrl.StartsWith("/logfile/") && resourceUrl.EndsWith(".txt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ namespace NzbDrone.Api.Logs
|
||||||
|
|
||||||
public LogFileModule(IAppFolderInfo appFolderInfo,
|
public LogFileModule(IAppFolderInfo appFolderInfo,
|
||||||
IDiskProvider diskProvider)
|
IDiskProvider diskProvider)
|
||||||
: base("log/files")
|
: base("log/file")
|
||||||
{
|
{
|
||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
|
|
|
@ -40,6 +40,9 @@
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="DDay.iCal">
|
||||||
|
<HintPath>..\packages\DDay.iCal.1.0.2.575\lib\DDay.iCal.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
<Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||||
<SpecificVersion>False</SpecificVersion>
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath>
|
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath>
|
||||||
|
@ -88,6 +91,7 @@
|
||||||
<Compile Include="Blacklist\BlacklistModule.cs" />
|
<Compile Include="Blacklist\BlacklistModule.cs" />
|
||||||
<Compile Include="Blacklist\BlacklistResource.cs" />
|
<Compile Include="Blacklist\BlacklistResource.cs" />
|
||||||
<Compile Include="Calendar\CalendarModule.cs" />
|
<Compile Include="Calendar\CalendarModule.cs" />
|
||||||
|
<Compile Include="Calendar\CalendarFeedModule.cs" />
|
||||||
<Compile Include="ClientSchema\SchemaDeserializer.cs" />
|
<Compile Include="ClientSchema\SchemaDeserializer.cs" />
|
||||||
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
|
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
|
||||||
<Compile Include="ClientSchema\Field.cs" />
|
<Compile Include="ClientSchema\Field.cs" />
|
||||||
|
@ -139,6 +143,7 @@
|
||||||
<Compile Include="Metadata\MetadataResource.cs" />
|
<Compile Include="Metadata\MetadataResource.cs" />
|
||||||
<Compile Include="Metadata\MetadataModule.cs" />
|
<Compile Include="Metadata\MetadataModule.cs" />
|
||||||
<Compile Include="Notifications\NotificationSchemaModule.cs" />
|
<Compile Include="Notifications\NotificationSchemaModule.cs" />
|
||||||
|
<Compile Include="NzbDroneFeedModule.cs" />
|
||||||
<Compile Include="ProviderResource.cs" />
|
<Compile Include="ProviderResource.cs" />
|
||||||
<Compile Include="ProviderModuleBase.cs" />
|
<Compile Include="ProviderModuleBase.cs" />
|
||||||
<Compile Include="Indexers\IndexerSchemaModule.cs" />
|
<Compile Include="Indexers\IndexerSchemaModule.cs" />
|
||||||
|
@ -199,7 +204,9 @@
|
||||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj">
|
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj">
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
using Nancy;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api
|
||||||
|
{
|
||||||
|
public abstract class NzbDroneFeedModule : NancyModule
|
||||||
|
{
|
||||||
|
protected NzbDroneFeedModule(string resource)
|
||||||
|
: base("/feed/" + resource.Trim('/'))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
|
<package id="DDay.iCal" version="1.0.2.575" targetFramework="net40" />
|
||||||
<package id="FluentValidation" version="5.0.0.1" targetFramework="net40" />
|
<package id="FluentValidation" version="5.0.0.1" targetFramework="net40" />
|
||||||
<package id="Nancy" version="0.21.1" targetFramework="net40" />
|
<package id="Nancy" version="0.21.1" targetFramework="net40" />
|
||||||
<package id="Nancy.Authentication.Basic" version="0.21.1" targetFramework="net40" />
|
<package id="Nancy.Authentication.Basic" version="0.21.1" targetFramework="net40" />
|
||||||
|
|
|
@ -95,6 +95,9 @@
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -82,6 +82,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -111,6 +111,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Properties\" />
|
<Folder Include="Properties\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace NzbDrone.Common
|
|
||||||
{
|
|
||||||
public static class DateTimeExtensions
|
|
||||||
{
|
|
||||||
public static DateTime GetFirstDayOfWeek(this DateTime dayInWeek)
|
|
||||||
{
|
|
||||||
var defaultCultureInfo = CultureInfo.CurrentCulture;
|
|
||||||
return GetFirstDayOfWeek(dayInWeek, defaultCultureInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DateTime GetFirstDayOfWeek(this DateTime dayInWeek, CultureInfo cultureInfo)
|
|
||||||
{
|
|
||||||
DayOfWeek firstDay = cultureInfo.DateTimeFormat.FirstDayOfWeek;
|
|
||||||
DateTime firstDayInWeek = dayInWeek.Date;
|
|
||||||
while (firstDayInWeek.DayOfWeek != firstDay)
|
|
||||||
firstDayInWeek = firstDayInWeek.AddDays(-1);
|
|
||||||
|
|
||||||
return firstDayInWeek;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -401,10 +401,15 @@ namespace NzbDrone.Common.Disk
|
||||||
{
|
{
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
var newAttributes = File.GetAttributes(path) & ~(FileAttributes.ReadOnly);
|
var attributes = File.GetAttributes(path);
|
||||||
|
|
||||||
|
if (attributes.HasFlag(FileAttributes.ReadOnly))
|
||||||
|
{
|
||||||
|
var newAttributes = attributes & ~(FileAttributes.ReadOnly);
|
||||||
File.SetAttributes(path, newAttributes);
|
File.SetAttributes(path, newAttributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public FileAttributes GetFileAttributes(string path)
|
public FileAttributes GetFileAttributes(string path)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace NzbDrone.Common.EnvironmentInfo
|
namespace NzbDrone.Common.EnvironmentInfo
|
||||||
|
@ -18,7 +19,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||||
IsLinux = IsMono && !IsOsx;
|
IsLinux = IsMono && !IsOsx;
|
||||||
IsWindows = !IsMono;
|
IsWindows = !IsMono;
|
||||||
|
|
||||||
FirstDayOfWeek = DateTime.Today.GetFirstDayOfWeek().DayOfWeek;
|
FirstDayOfWeek = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
|
||||||
|
|
||||||
if (!IsMono)
|
if (!IsMono)
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Extensions
|
||||||
|
{
|
||||||
|
public static class StreamExtensions
|
||||||
|
{
|
||||||
|
public static byte[] ToBytes(this Stream input)
|
||||||
|
{
|
||||||
|
var buffer = new byte[16 * 1024];
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
int read;
|
||||||
|
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
ms.Write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,7 +66,6 @@
|
||||||
<Compile Include="Composition\Container.cs" />
|
<Compile Include="Composition\Container.cs" />
|
||||||
<Compile Include="Composition\IContainer.cs" />
|
<Compile Include="Composition\IContainer.cs" />
|
||||||
<Compile Include="Composition\ContainerBuilderBase.cs" />
|
<Compile Include="Composition\ContainerBuilderBase.cs" />
|
||||||
<Compile Include="DateTimeExtensions.cs" />
|
|
||||||
<Compile Include="DictionaryExtensions.cs" />
|
<Compile Include="DictionaryExtensions.cs" />
|
||||||
<Compile Include="Disk\DiskProviderBase.cs" />
|
<Compile Include="Disk\DiskProviderBase.cs" />
|
||||||
<Compile Include="EnsureThat\Ensure.cs" />
|
<Compile Include="EnsureThat\Ensure.cs" />
|
||||||
|
@ -105,8 +104,10 @@
|
||||||
<Compile Include="PathEqualityComparer.cs" />
|
<Compile Include="PathEqualityComparer.cs" />
|
||||||
<Compile Include="Processes\INzbDroneProcessProvider.cs" />
|
<Compile Include="Processes\INzbDroneProcessProvider.cs" />
|
||||||
<Compile Include="Processes\ProcessOutput.cs" />
|
<Compile Include="Processes\ProcessOutput.cs" />
|
||||||
|
<Compile Include="RateGate.cs" />
|
||||||
<Compile Include="Serializer\IntConverter.cs" />
|
<Compile Include="Serializer\IntConverter.cs" />
|
||||||
<Compile Include="Services.cs" />
|
<Compile Include="Services.cs" />
|
||||||
|
<Compile Include="Extensions\StreamExtensions.cs" />
|
||||||
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
|
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
|
||||||
<Compile Include="Security\IgnoreCertErrorPolicy.cs" />
|
<Compile Include="Security\IgnoreCertErrorPolicy.cs" />
|
||||||
<Compile Include="StringExtensions.cs" />
|
<Compile Include="StringExtensions.cs" />
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
/*
|
||||||
|
* Code from: http://www.jackleitch.net/2010/10/better-rate-limiting-with-dot-net/
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to control the rate of some occurrence per unit of time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// To control the rate of an action using a <see cref="RateGate"/>,
|
||||||
|
/// code should simply call <see cref="WaitToProceed()"/> prior to
|
||||||
|
/// performing the action. <see cref="WaitToProceed()"/> will block
|
||||||
|
/// the current thread until the action is allowed based on the rate
|
||||||
|
/// limit.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This class is thread safe. A single <see cref="RateGate"/> instance
|
||||||
|
/// may be used to control the rate of an occurrence across multiple
|
||||||
|
/// threads.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class RateGate : IDisposable
|
||||||
|
{
|
||||||
|
// Semaphore used to count and limit the number of occurrences per
|
||||||
|
// unit time.
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
|
||||||
|
// Times (in millisecond ticks) at which the semaphore should be exited.
|
||||||
|
private readonly ConcurrentQueue<int> _exitTimes;
|
||||||
|
|
||||||
|
// Timer used to trigger exiting the semaphore.
|
||||||
|
private readonly Timer _exitTimer;
|
||||||
|
|
||||||
|
// Whether this instance is disposed.
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of occurrences allowed per unit of time.
|
||||||
|
/// </summary>
|
||||||
|
public int Occurrences { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The length of the time unit, in milliseconds.
|
||||||
|
/// </summary>
|
||||||
|
public int TimeUnitMilliseconds { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a <see cref="RateGate"/> with a rate of <paramref name="occurrences"/>
|
||||||
|
/// per <paramref name="timeUnit"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="occurrences">Number of occurrences allowed per unit of time.</param>
|
||||||
|
/// <param name="timeUnit">Length of the time unit.</param>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">
|
||||||
|
/// If <paramref name="occurrences"/> or <paramref name="timeUnit"/> is negative.
|
||||||
|
/// </exception>
|
||||||
|
public RateGate(int occurrences, TimeSpan timeUnit)
|
||||||
|
{
|
||||||
|
// Check the arguments.
|
||||||
|
if (occurrences <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException("occurrences", "Number of occurrences must be a positive integer");
|
||||||
|
if (timeUnit != timeUnit.Duration())
|
||||||
|
throw new ArgumentOutOfRangeException("timeUnit", "Time unit must be a positive span of time");
|
||||||
|
if (timeUnit >= TimeSpan.FromMilliseconds(UInt32.MaxValue))
|
||||||
|
throw new ArgumentOutOfRangeException("timeUnit", "Time unit must be less than 2^32 milliseconds");
|
||||||
|
|
||||||
|
Occurrences = occurrences;
|
||||||
|
TimeUnitMilliseconds = (int)timeUnit.TotalMilliseconds;
|
||||||
|
|
||||||
|
// Create the semaphore, with the number of occurrences as the maximum count.
|
||||||
|
_semaphore = new SemaphoreSlim(Occurrences, Occurrences);
|
||||||
|
|
||||||
|
// Create a queue to hold the semaphore exit times.
|
||||||
|
_exitTimes = new ConcurrentQueue<int>();
|
||||||
|
|
||||||
|
// Create a timer to exit the semaphore. Use the time unit as the original
|
||||||
|
// interval length because that's the earliest we will need to exit the semaphore.
|
||||||
|
_exitTimer = new Timer(ExitTimerCallback, null, TimeUnitMilliseconds, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback for the exit timer that exits the semaphore based on exit times
|
||||||
|
// in the queue and then sets the timer for the nextexit time.
|
||||||
|
private void ExitTimerCallback(object state)
|
||||||
|
{
|
||||||
|
// While there are exit times that are passed due still in the queue,
|
||||||
|
// exit the semaphore and dequeue the exit time.
|
||||||
|
int exitTime;
|
||||||
|
while (_exitTimes.TryPeek(out exitTime)
|
||||||
|
&& unchecked(exitTime - Environment.TickCount) <= 0)
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
_exitTimes.TryDequeue(out exitTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the next exit time from the queue and compute
|
||||||
|
// the time until the next check should take place. If the
|
||||||
|
// queue is empty, then no exit times will occur until at least
|
||||||
|
// one time unit has passed.
|
||||||
|
int timeUntilNextCheck;
|
||||||
|
if (_exitTimes.TryPeek(out exitTime))
|
||||||
|
timeUntilNextCheck = unchecked(exitTime - Environment.TickCount);
|
||||||
|
else
|
||||||
|
timeUntilNextCheck = TimeUnitMilliseconds;
|
||||||
|
|
||||||
|
// Set the timer.
|
||||||
|
_exitTimer.Change(timeUntilNextCheck, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks the current thread until allowed to proceed or until the
|
||||||
|
/// specified timeout elapses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="millisecondsTimeout">Number of milliseconds to wait, or -1 to wait indefinitely.</param>
|
||||||
|
/// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
|
||||||
|
public bool WaitToProceed(int millisecondsTimeout)
|
||||||
|
{
|
||||||
|
// Check the arguments.
|
||||||
|
if (millisecondsTimeout < -1)
|
||||||
|
throw new ArgumentOutOfRangeException("millisecondsTimeout");
|
||||||
|
|
||||||
|
CheckDisposed();
|
||||||
|
|
||||||
|
// Block until we can enter the semaphore or until the timeout expires.
|
||||||
|
var entered = _semaphore.Wait(millisecondsTimeout);
|
||||||
|
|
||||||
|
// If we entered the semaphore, compute the corresponding exit time
|
||||||
|
// and add it to the queue.
|
||||||
|
if (entered)
|
||||||
|
{
|
||||||
|
var timeToExit = unchecked(Environment.TickCount + TimeUnitMilliseconds);
|
||||||
|
_exitTimes.Enqueue(timeToExit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks the current thread until allowed to proceed or until the
|
||||||
|
/// specified timeout elapses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeout"></param>
|
||||||
|
/// <returns>true if the thread is allowed to proceed, or false if timed out</returns>
|
||||||
|
public bool WaitToProceed(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
return WaitToProceed((int)timeout.TotalMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks the current thread indefinitely until allowed to proceed.
|
||||||
|
/// </summary>
|
||||||
|
public void WaitToProceed()
|
||||||
|
{
|
||||||
|
WaitToProceed(Timeout.Infinite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throws an ObjectDisposedException if this object is disposed.
|
||||||
|
private void CheckDisposed()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException("RateGate is already disposed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases unmanaged resources held by an instance of this class.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases unmanaged resources held by an instance of this class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isDisposing">Whether this object is being disposed.</param>
|
||||||
|
protected virtual void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
if (!_isDisposed)
|
||||||
|
{
|
||||||
|
if (isDisposing)
|
||||||
|
{
|
||||||
|
// The semaphore and timer both implement IDisposable and
|
||||||
|
// therefore must be disposed.
|
||||||
|
_semaphore.Dispose();
|
||||||
|
_exitTimer.Dispose();
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.Clients.Nzbget;
|
using NzbDrone.Core.Download.Clients.Nzbget;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
@ -46,16 +48,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_add_item_to_queue()
|
public void should_add_item_to_queue()
|
||||||
{
|
{
|
||||||
var p = new object[] {"30.Rock.S01E01.Pilot.720p.hdtv.nzb", "TV", 50, false, "http://www.nzbdrone.com"};
|
|
||||||
|
|
||||||
Mocker.GetMock<INzbgetProxy>()
|
Mocker.GetMock<INzbgetProxy>()
|
||||||
.Setup(s => s.AddNzb(It.IsAny<NzbgetSettings>(), p))
|
.Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()))
|
||||||
.Returns(true);
|
.Returns("id");
|
||||||
|
|
||||||
Subject.DownloadNzb(_remoteEpisode);
|
Subject.DownloadNzb(_remoteEpisode);
|
||||||
|
|
||||||
Mocker.GetMock<INzbgetProxy>()
|
Mocker.GetMock<INzbgetProxy>()
|
||||||
.Verify(v => v.AddNzb(It.IsAny<NzbgetSettings>(), It.IsAny<object []>()), Times.Once());
|
.Verify(v => v.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()), Times.Once());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
|
||||||
_queue = Builder<NzbgetQueueItem>.CreateListOfSize(5)
|
_queue = Builder<NzbgetQueueItem>.CreateListOfSize(5)
|
||||||
.All()
|
.All()
|
||||||
.With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb")
|
.With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb")
|
||||||
|
.With(q => q.Parameters = new List<NzbgetParameter>
|
||||||
|
{
|
||||||
|
new NzbgetParameter { Name = "drone", Value = "id" }
|
||||||
|
})
|
||||||
.Build()
|
.Build()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Metadata;
|
||||||
|
using NzbDrone.Core.Metadata.Files;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_not_delete_metadata_files_when_they_are_for_the_same_series_but_different_consumers()
|
||||||
|
{
|
||||||
|
var files = Builder<MetadataFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Type = MetadataType.SeriesMetadata)
|
||||||
|
.With(m => m.SeriesId = 1)
|
||||||
|
.BuildListOfNew();
|
||||||
|
|
||||||
|
Db.InsertMany(files);
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Count.Should().Be(files.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_delete_metadata_files_for_different_series()
|
||||||
|
{
|
||||||
|
var files = Builder<MetadataFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Type = MetadataType.SeriesMetadata)
|
||||||
|
.With(m => m.Consumer = "XbmcMetadata")
|
||||||
|
.BuildListOfNew();
|
||||||
|
|
||||||
|
Db.InsertMany(files);
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Count.Should().Be(files.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_delete_metadata_files_when_they_are_for_the_same_series_and_consumer()
|
||||||
|
{
|
||||||
|
var files = Builder<MetadataFile>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(m => m.Type = MetadataType.SeriesMetadata)
|
||||||
|
.With(m => m.SeriesId = 1)
|
||||||
|
.With(m => m.Consumer = "XbmcMetadata")
|
||||||
|
.BuildListOfNew();
|
||||||
|
|
||||||
|
Db.InsertMany(files);
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Count.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer()
|
||||||
|
{
|
||||||
|
var file = Builder<MetadataFile>.CreateNew()
|
||||||
|
.BuildNew();
|
||||||
|
|
||||||
|
Db.Insert(file);
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Count.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Practices.ObjectBuilder2;
|
using Microsoft.Practices.ObjectBuilder2;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
using NzbDrone.Core.Jobs;
|
using NzbDrone.Core.Jobs;
|
||||||
using NzbDrone.Core.Organizer;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||||
|
|
|
@ -136,6 +136,7 @@
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
|
||||||
|
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
||||||
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
|
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
|
||||||
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
|
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
|
||||||
|
@ -231,6 +232,7 @@
|
||||||
<Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
|
<Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
|
||||||
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
|
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
|
||||||
<Compile Include="TvTests\SeriesServiceTests\UpdateSeriesFixture.cs" />
|
<Compile Include="TvTests\SeriesServiceTests\UpdateSeriesFixture.cs" />
|
||||||
|
<Compile Include="TvTests\ShouldRefreshSeriesFixture.cs" />
|
||||||
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
|
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
||||||
<Compile Include="Qualities\QualityDefinitionServiceFixture.cs" />
|
<Compile Include="Qualities\QualityDefinitionServiceFixture.cs" />
|
||||||
|
@ -374,6 +376,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="ProviderTests\UpdateProviderTests\" />
|
<Folder Include="ProviderTests\UpdateProviderTests\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PreBuildEvent>
|
<PreBuildEvent>
|
||||||
|
|
|
@ -27,6 +27,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||||
[TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")]
|
[TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")]
|
||||||
[TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")]
|
[TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")]
|
||||||
[TestCase("THIS SHOULD NEVER PARSE")]
|
[TestCase("THIS SHOULD NEVER PARSE")]
|
||||||
|
[TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")]
|
||||||
public void should_not_parse_crap(string title)
|
public void should_not_parse_crap(string title)
|
||||||
{
|
{
|
||||||
Parser.Parser.ParseTitle(title).Should().BeNull();
|
Parser.Parser.ParseTitle(title).Should().BeNull();
|
||||||
|
|
|
@ -31,6 +31,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||||
[TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)]
|
[TestCase(@"C:\Test\Unsorted\The.Big.Bang.Theory.S01E01.720p.HDTV\tbbt101.avi", 1, 1)]
|
||||||
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)]
|
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E19.720p.BluRay.x264-SiNNERS-RP\ba27283b17c00d01193eacc02a8ba98eeb523a76.mkv", 2, 19)]
|
||||||
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)]
|
[TestCase(@"C:\Test\Unsorted\Terminator.The.Sarah.Connor.Chronicles.S02E18.720p.BluRay.x264-SiNNERS-RP\45a55debe3856da318cc35882ad07e43cd32fd15.mkv", 2, 18)]
|
||||||
|
[TestCase(@"C:\Test\The.Blacklist.S01E16.720p.HDTV.X264-DIMENSION\XRmZciqkBopq4851Ddbipe\Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv", 1, 16)]
|
||||||
public void should_parse_from_path(string path, int season, int episode)
|
public void should_parse_from_path(string path, int season, int episode)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParsePath(path);
|
var result = Parser.Parser.ParsePath(path);
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||||
[TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)]
|
[TestCase("The Office Season4 WS PDTV XviD FUtV", "The Office", 4)]
|
||||||
[TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)]
|
[TestCase("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)]
|
||||||
[TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)]
|
[TestCase("Doctor Who Confidential Season 3", "Doctor Who Confidential", 3)]
|
||||||
|
[TestCase("Fleming.S01.720p.WEBDL.DD5.1.H.264-NTb", "Fleming", 1)]
|
||||||
public void should_parsefull_season_release(string postTitle, string title, int season)
|
public void should_parsefull_season_release(string postTitle, string title, int season)
|
||||||
{
|
{
|
||||||
var result = Parser.Parser.ParseTitle(postTitle);
|
var result = Parser.Parser.ParseTitle(postTitle);
|
||||||
|
|
|
@ -4,10 +4,10 @@ using System.IO;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common;
|
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Core.RootFolders;
|
using NzbDrone.Core.RootFolders;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.RootFolderTests
|
namespace NzbDrone.Core.Test.RootFolderTests
|
||||||
|
@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
||||||
.Returns(new List<RootFolder>());
|
.Returns(new List<RootFolder>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WithNoneExistingFolder()
|
private void WithNonExistingFolder()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(m => m.FolderExists(It.IsAny<string>()))
|
.Setup(m => m.FolderExists(It.IsAny<string>()))
|
||||||
|
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_throw_if_folder_being_added_doesnt_exist()
|
public void should_throw_if_folder_being_added_doesnt_exist()
|
||||||
{
|
{
|
||||||
WithNoneExistingFolder();
|
WithNonExistingFolder();
|
||||||
|
|
||||||
Assert.Throws<DirectoryNotFoundException>(() => Subject.Add(new RootFolder { Path = "C:\\TEST".AsOsAgnostic() }));
|
Assert.Throws<DirectoryNotFoundException>(() => Subject.Add(new RootFolder { Path = "C:\\TEST".AsOsAgnostic() }));
|
||||||
}
|
}
|
||||||
|
@ -62,9 +62,9 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void None_existing_folder_returns_empty_list()
|
public void should_return_empty_list_when_folder_doesnt_exist()
|
||||||
{
|
{
|
||||||
WithNoneExistingFolder();
|
WithNonExistingFolder();
|
||||||
|
|
||||||
Mocker.GetMock<IRootFolderRepository>().Setup(c => c.All()).Returns(new List<RootFolder>());
|
Mocker.GetMock<IRootFolderRepository>().Setup(c => c.All()).Returns(new List<RootFolder>());
|
||||||
|
|
||||||
|
@ -100,5 +100,26 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
||||||
|
|
||||||
Assert.Throws<InvalidOperationException>(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() }));
|
Assert.Throws<InvalidOperationException>(() => Subject.Add(new RootFolder { Path = @"C:\TV".AsOsAgnostic() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_include_system_files_and_folders()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(s => s.GetDirectories(It.IsAny<String>()))
|
||||||
|
.Returns(new string[]
|
||||||
|
{
|
||||||
|
@"C:\30 Rock".AsOsAgnostic(),
|
||||||
|
@"C:\$Recycle.Bin".AsOsAgnostic(),
|
||||||
|
@"C:\.AppleDouble".AsOsAgnostic(),
|
||||||
|
@"C:\Test\.AppleDouble".AsOsAgnostic()
|
||||||
|
});
|
||||||
|
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Setup(s => s.GetAllSeries())
|
||||||
|
.Returns(new List<Series>());
|
||||||
|
|
||||||
|
Subject.GetUnmappedFolders(@"C:\")
|
||||||
|
.Should().OnlyContain(u => u.Path == @"C:\30 Rock".AsOsAgnostic());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.TvTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ShouldRefreshSeriesFixture : TestBase<ShouldRefreshSeries>
|
||||||
|
{
|
||||||
|
private Series _series;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_series = Builder<Series>.CreateNew()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenSeriesIsEnded()
|
||||||
|
{
|
||||||
|
_series.Status = SeriesStatusType.Ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenSeriesLastRefreshedRecently()
|
||||||
|
{
|
||||||
|
_series.LastInfoSync = DateTime.UtcNow.AddDays(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_series_is_continuing()
|
||||||
|
{
|
||||||
|
_series.Status = SeriesStatusType.Continuing;
|
||||||
|
|
||||||
|
Subject.ShouldRefresh(_series).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_series_last_refreshed_more_than_30_days_ago()
|
||||||
|
{
|
||||||
|
GivenSeriesIsEnded();
|
||||||
|
_series.LastInfoSync = DateTime.UtcNow.AddDays(-100);
|
||||||
|
|
||||||
|
Subject.ShouldRefresh(_series).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_should_return_true_if_episode_aired_in_last_30_days()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(s => s.GetEpisodeBySeries(_series.Id))
|
||||||
|
.Returns(Builder<Episode>.CreateListOfSize(2)
|
||||||
|
.TheFirst(1)
|
||||||
|
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-7))
|
||||||
|
.TheLast(1)
|
||||||
|
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-100))
|
||||||
|
.Build()
|
||||||
|
.ToList());
|
||||||
|
|
||||||
|
Subject.ShouldRefresh(_series).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_should_return_false_when_recently_refreshed_ended_show_has_not_aired_for_30_days()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IEpisodeService>()
|
||||||
|
.Setup(s => s.GetEpisodeBySeries(_series.Id))
|
||||||
|
.Returns(Builder<Episode>.CreateListOfSize(2)
|
||||||
|
.All()
|
||||||
|
.With(e => e.AirDateUtc = DateTime.Today.AddDays(-100))
|
||||||
|
.Build()
|
||||||
|
.ToList());
|
||||||
|
|
||||||
|
Subject.ShouldRefresh(_series).Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ namespace NzbDrone.Core.Datastore
|
||||||
IDatabase Create(MigrationType migrationType = MigrationType.Main);
|
IDatabase Create(MigrationType migrationType = MigrationType.Main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class DbFactory : IDbFactory
|
public class DbFactory : IDbFactory
|
||||||
{
|
{
|
||||||
private readonly IMigrationController _migrationController;
|
private readonly IMigrationController _migrationController;
|
||||||
|
@ -79,8 +78,11 @@ namespace NzbDrone.Core.Datastore
|
||||||
return dataMapper;
|
return dataMapper;
|
||||||
});
|
});
|
||||||
|
|
||||||
db.Vacuum();
|
|
||||||
|
|
||||||
|
if (migrationType == MigrationType.Main)
|
||||||
|
{
|
||||||
|
db.Vacuum();
|
||||||
|
}
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,11 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
|
|
||||||
if (decision != null)
|
if (decision != null)
|
||||||
{
|
{
|
||||||
|
if (decision.Rejections.Any())
|
||||||
|
{
|
||||||
|
_logger.Debug("Release rejected for the following reasons: {0}", String.Join(", ", decision.Rejections));
|
||||||
|
}
|
||||||
|
|
||||||
yield return decision;
|
yield return decision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
|
@ -6,10 +7,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
private string _nzbName;
|
private string _nzbName;
|
||||||
public Int32 NzbId { get; set; }
|
public Int32 NzbId { get; set; }
|
||||||
|
public Int32 FirstId { get; set; }
|
||||||
|
public Int32 LastId { get; set; }
|
||||||
public string NzbName { get; set; }
|
public string NzbName { get; set; }
|
||||||
public String Category { get; set; }
|
public String Category { get; set; }
|
||||||
public Int32 FileSizeMb { get; set; }
|
public Int32 FileSizeMb { get; set; }
|
||||||
public Int32 RemainingSizeMb { get; set; }
|
public Int32 RemainingSizeMb { get; set; }
|
||||||
public Int32 PausedSizeMb { get; set; }
|
public Int32 PausedSizeMb { get; set; }
|
||||||
|
public List<NzbgetParameter> Parameters { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
@ -13,14 +14,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
private readonly INzbgetProxy _proxy;
|
private readonly INzbgetProxy _proxy;
|
||||||
private readonly IParsingService _parsingService;
|
private readonly IParsingService _parsingService;
|
||||||
|
private readonly IHttpProvider _httpProvider;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public Nzbget(INzbgetProxy proxy,
|
public Nzbget(INzbgetProxy proxy,
|
||||||
IParsingService parsingService,
|
IParsingService parsingService,
|
||||||
|
IHttpProvider httpProvider,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
_parsingService = parsingService;
|
_parsingService = parsingService;
|
||||||
|
_httpProvider = httpProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,16 +33,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
var url = remoteEpisode.Release.DownloadUrl;
|
var url = remoteEpisode.Release.DownloadUrl;
|
||||||
var title = remoteEpisode.Release.Title + ".nzb";
|
var title = remoteEpisode.Release.Title + ".nzb";
|
||||||
|
|
||||||
string cat = Settings.TvCategory;
|
string category = Settings.TvCategory;
|
||||||
int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
|
int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
|
||||||
|
|
||||||
_logger.Info("Adding report [{0}] to the queue.", title);
|
_logger.Info("Adding report [{0}] to the queue.", title);
|
||||||
|
|
||||||
var success = _proxy.AddNzb(Settings, title, cat, priority, false, url);
|
using (var nzb = _httpProvider.DownloadStream(url))
|
||||||
|
{
|
||||||
|
_logger.Info("Adding report [{0}] to the queue.", title);
|
||||||
|
var response = _proxy.DownloadNzb(nzb, title, category, priority, Settings);
|
||||||
|
|
||||||
_logger.Debug("Queue Response: [{0}]", success);
|
return response;
|
||||||
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<QueueItem> GetQueue()
|
public override IEnumerable<QueueItem> GetQueue()
|
||||||
|
@ -57,14 +63,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
|
|
||||||
var queueItems = new List<QueueItem>();
|
var queueItems = new List<QueueItem>();
|
||||||
|
|
||||||
foreach (var nzbGetQueueItem in queue)
|
foreach (var item in queue)
|
||||||
{
|
{
|
||||||
|
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
|
||||||
|
|
||||||
var queueItem = new QueueItem();
|
var queueItem = new QueueItem();
|
||||||
queueItem.Id = nzbGetQueueItem.NzbId.ToString();
|
queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
|
||||||
queueItem.Title = nzbGetQueueItem.NzbName;
|
queueItem.Title = item.NzbName;
|
||||||
queueItem.Size = nzbGetQueueItem.FileSizeMb;
|
queueItem.Size = item.FileSizeMb;
|
||||||
queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb;
|
queueItem.Sizeleft = item.RemainingSizeMb;
|
||||||
queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued";
|
queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued";
|
||||||
|
|
||||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title);
|
var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title);
|
||||||
if (parsedEpisodeInfo == null) continue;
|
if (parsedEpisodeInfo == null) continue;
|
||||||
|
@ -81,7 +89,43 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
|
|
||||||
public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10)
|
public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10)
|
||||||
{
|
{
|
||||||
return new HistoryItem[0];
|
List<NzbgetHistoryItem> history;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
history = _proxy.GetHistory(Settings);
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
_logger.ErrorException(ex.Message, ex);
|
||||||
|
return Enumerable.Empty<HistoryItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var historyItems = new List<HistoryItem>();
|
||||||
|
var successStatues = new[] {"SUCCESS", "NONE"};
|
||||||
|
|
||||||
|
foreach (var item in history)
|
||||||
|
{
|
||||||
|
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
|
||||||
|
var status = successStatues.Contains(item.ParStatus) &&
|
||||||
|
successStatues.Contains(item.ScriptStatus)
|
||||||
|
? HistoryStatus.Completed
|
||||||
|
: HistoryStatus.Failed;
|
||||||
|
|
||||||
|
var historyItem = new HistoryItem();
|
||||||
|
historyItem.Id = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
|
||||||
|
historyItem.Title = item.Name;
|
||||||
|
historyItem.Size = item.FileSizeMb.ToString(); //Why is this a string?
|
||||||
|
historyItem.DownloadTime = 0;
|
||||||
|
historyItem.Storage = item.DestDir;
|
||||||
|
historyItem.Category = item.Category;
|
||||||
|
historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus);
|
||||||
|
historyItem.Status = status;
|
||||||
|
|
||||||
|
historyItems.Add(historyItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveFromQueue(string id)
|
public override void RemoveFromQueue(string id)
|
||||||
|
@ -91,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
|
|
||||||
public override void RemoveFromHistory(string id)
|
public override void RemoveFromHistory(string id)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
_proxy.RemoveFromHistory(id, Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Test()
|
public override void Test()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
public class EnqueueResponse
|
public class NzbgetBooleanResponse
|
||||||
{
|
{
|
||||||
public String Version { get; set; }
|
public String Version { get; set; }
|
||||||
public Boolean Result { get; set; }
|
public Boolean Result { get; set; }
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
|
{
|
||||||
|
public class NzbgetHistoryItem
|
||||||
|
{
|
||||||
|
private string _nzbName;
|
||||||
|
public Int32 Id { get; set; }
|
||||||
|
public String Name { get; set; }
|
||||||
|
public String Category { get; set; }
|
||||||
|
public Int32 FileSizeMb { get; set; }
|
||||||
|
public String ParStatus { get; set; }
|
||||||
|
public String ScriptStatus { get; set; }
|
||||||
|
public String DestDir { get; set; }
|
||||||
|
public List<NzbgetParameter> Parameters { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,11 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
public class NzbgetQueue
|
public class NzbgetListResponse<T>
|
||||||
{
|
{
|
||||||
public String Version { get; set; }
|
public String Version { get; set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "result")]
|
[JsonProperty(PropertyName = "result")]
|
||||||
public List<NzbgetQueueItem> QueueItems { get; set; }
|
public List<T> QueueItems { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
|
{
|
||||||
|
public class NzbgetParameter
|
||||||
|
{
|
||||||
|
public String Name { get; set; }
|
||||||
|
public object Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Core.Rest;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
@ -9,9 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
public interface INzbgetProxy
|
public interface INzbgetProxy
|
||||||
{
|
{
|
||||||
bool AddNzb(NzbgetSettings settings, params object[] parameters);
|
string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings);
|
||||||
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
|
List<NzbgetQueueItem> GetQueue(NzbgetSettings settings);
|
||||||
|
List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings);
|
||||||
VersionResponse GetVersion(NzbgetSettings settings);
|
VersionResponse GetVersion(NzbgetSettings settings);
|
||||||
|
void RemoveFromHistory(string id, NzbgetSettings settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NzbgetProxy : INzbgetProxy
|
public class NzbgetProxy : INzbgetProxy
|
||||||
|
@ -23,18 +28,50 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AddNzb(NzbgetSettings settings, params object[] parameters)
|
public string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("appendurl", parameters));
|
var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzb.ToBytes()) };
|
||||||
|
var request = BuildRequest(new JsonRequest("append", parameters));
|
||||||
|
|
||||||
return Json.Deserialize<EnqueueResponse>(ProcessRequest(request, settings)).Result;
|
var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings));
|
||||||
|
_logger.Debug("Queue Response: [{0}]", response.Result);
|
||||||
|
|
||||||
|
if (!response.Result)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var queue = GetQueue(settings);
|
||||||
|
var item = queue.FirstOrDefault(q => q.NzbName == title.Substring(0, title.Length - 4));
|
||||||
|
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var droneId = Guid.NewGuid().ToString().Replace("-", "");
|
||||||
|
var editResult = EditQueue("GroupSetParameter", 0, "drone=" + droneId, item.LastId, settings);
|
||||||
|
|
||||||
|
if (editResult)
|
||||||
|
{
|
||||||
|
_logger.Debug("Nzbget download drone parameter set to: {0}", droneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return droneId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings)
|
public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("listgroups"));
|
var request = BuildRequest(new JsonRequest("listgroups"));
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetQueue>(ProcessRequest(request, settings)).QueueItems;
|
return Json.Deserialize<NzbgetListResponse<NzbgetQueueItem>>(ProcessRequest(request, settings)).QueueItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(new JsonRequest("history"));
|
||||||
|
|
||||||
|
return Json.Deserialize<NzbgetListResponse<NzbgetHistoryItem>>(ProcessRequest(request, settings)).QueueItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VersionResponse GetVersion(NzbgetSettings settings)
|
public VersionResponse GetVersion(NzbgetSettings settings)
|
||||||
|
@ -44,6 +81,32 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
return Json.Deserialize<VersionResponse>(ProcessRequest(request, settings));
|
return Json.Deserialize<VersionResponse>(ProcessRequest(request, settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RemoveFromHistory(string id, NzbgetSettings settings)
|
||||||
|
{
|
||||||
|
var history = GetHistory(settings);
|
||||||
|
var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null);
|
||||||
|
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
_logger.Warn("Unable to remove item from nzbget's history, Unknown ID: {0}", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EditQueue("HistoryDelete", 0, "", item.Id, settings))
|
||||||
|
{
|
||||||
|
_logger.Warn("Failed to remove item from nzbget history, {0} [{1}]", item.Name, item.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings)
|
||||||
|
{
|
||||||
|
var parameters = new object[] { command, offset, editText, id };
|
||||||
|
var request = BuildRequest(new JsonRequest("editqueue", parameters));
|
||||||
|
var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings));
|
||||||
|
|
||||||
|
return response.Result;
|
||||||
|
}
|
||||||
|
|
||||||
private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings)
|
private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var client = BuildClient(settings);
|
var client = BuildClient(settings);
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.IO;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
|
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
|
||||||
using NzbDrone.Core.Instrumentation.Extensions;
|
using NzbDrone.Core.Instrumentation.Extensions;
|
||||||
|
@ -35,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||||
var request = new RestRequest(Method.POST);
|
var request = new RestRequest(Method.POST);
|
||||||
var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority);
|
var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority);
|
||||||
|
|
||||||
request.AddFile("name", ReadFully(nzb), title, "application/x-nzb");
|
request.AddFile("name", nzb.ToBytes(), title, "application/x-nzb");
|
||||||
|
|
||||||
SabnzbdAddResponse response;
|
SabnzbdAddResponse response;
|
||||||
|
|
||||||
|
@ -161,20 +162,5 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||||
if (result.Failed)
|
if (result.Failed)
|
||||||
throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error);
|
throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Find a better home for this
|
|
||||||
private byte[] ReadFully(Stream input)
|
|
||||||
{
|
|
||||||
byte[] buffer = new byte[16 * 1024];
|
|
||||||
using (MemoryStream ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
int read;
|
|
||||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
|
||||||
{
|
|
||||||
ms.Write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||||
|
{
|
||||||
|
public class CleanupDuplicateMetadataFiles : IHousekeepingTask
|
||||||
|
{
|
||||||
|
private readonly IDatabase _database;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public CleanupDuplicateMetadataFiles(IDatabase database, Logger logger)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clean()
|
||||||
|
{
|
||||||
|
_logger.Debug("Running cleanup of duplicate metadata files");
|
||||||
|
|
||||||
|
DeleteDuplicateSeriesMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteDuplicateSeriesMetadata()
|
||||||
|
{
|
||||||
|
var mapper = _database.GetDataMapper();
|
||||||
|
|
||||||
|
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
|
||||||
|
WHERE Id IN (
|
||||||
|
SELECT Id FROM MetadataFiles
|
||||||
|
WHERE Type = 1
|
||||||
|
GROUP BY SeriesId, Consumer
|
||||||
|
HAVING COUNT(SeriesId) > 1
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.IndexerSearch
|
namespace NzbDrone.Core.IndexerSearch
|
||||||
{
|
{
|
||||||
public class EpisodeSearchCommand : Command
|
public class MissingEpisodeSearchCommand : Command
|
||||||
{
|
{
|
||||||
public List<int> EpisodeIds { get; set; }
|
public List<int> EpisodeIds { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,30 @@
|
||||||
using NLog;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Instrumentation.Extensions;
|
using NzbDrone.Core.Instrumentation.Extensions;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
namespace NzbDrone.Core.IndexerSearch
|
namespace NzbDrone.Core.IndexerSearch
|
||||||
{
|
{
|
||||||
public class EpisodeSearchService : IExecute<EpisodeSearchCommand>
|
public class MissingEpisodeSearchService : IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand>
|
||||||
{
|
{
|
||||||
private readonly ISearchForNzb _nzbSearchService;
|
private readonly ISearchForNzb _nzbSearchService;
|
||||||
private readonly IDownloadApprovedReports _downloadApprovedReports;
|
private readonly IDownloadApprovedReports _downloadApprovedReports;
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public EpisodeSearchService(ISearchForNzb nzbSearchService,
|
public MissingEpisodeSearchService(ISearchForNzb nzbSearchService,
|
||||||
IDownloadApprovedReports downloadApprovedReports,
|
IDownloadApprovedReports downloadApprovedReports,
|
||||||
|
IEpisodeService episodeService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_nzbSearchService = nzbSearchService;
|
_nzbSearchService = nzbSearchService;
|
||||||
_downloadApprovedReports = downloadApprovedReports;
|
_downloadApprovedReports = downloadApprovedReports;
|
||||||
|
_episodeService = episodeService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,5 +38,39 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
_logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count);
|
_logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Execute(MissingEpisodeSearchCommand message)
|
||||||
|
{
|
||||||
|
//TODO: Look at ways to make this more efficient (grouping by series/season)
|
||||||
|
|
||||||
|
var episodes =
|
||||||
|
_episodeService.EpisodesWithoutFiles(new PagingSpec<Episode>
|
||||||
|
{
|
||||||
|
Page = 1,
|
||||||
|
PageSize = 100000,
|
||||||
|
SortDirection = SortDirection.Ascending,
|
||||||
|
SortKey = "Id",
|
||||||
|
FilterExpression = v => v.Monitored && v.Series.Monitored
|
||||||
|
}).Records.ToList();
|
||||||
|
|
||||||
|
_logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count);
|
||||||
|
var downloadedCount = 0;
|
||||||
|
|
||||||
|
//Limit requests to indexers at 100 per minute
|
||||||
|
using (var rateGate = new RateGate(100, TimeSpan.FromSeconds(60)))
|
||||||
|
{
|
||||||
|
foreach (var episode in episodes)
|
||||||
|
{
|
||||||
|
rateGate.WaitToProceed();
|
||||||
|
var decisions = _nzbSearchService.EpisodeSearch(episode);
|
||||||
|
var downloaded = _downloadApprovedReports.DownloadApproved(decisions);
|
||||||
|
downloadedCount += downloaded.Count;
|
||||||
|
|
||||||
|
_logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", episodes.Count, downloadedCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.IndexerSearch
|
||||||
|
{
|
||||||
|
public class EpisodeSearchCommand : Command
|
||||||
|
{
|
||||||
|
public List<int> EpisodeIds { get; set; }
|
||||||
|
|
||||||
|
public override bool SendUpdatesToClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
public interface ISearchForNzb
|
public interface ISearchForNzb
|
||||||
{
|
{
|
||||||
List<DownloadDecision> EpisodeSearch(int episodeId);
|
List<DownloadDecision> EpisodeSearch(int episodeId);
|
||||||
|
List<DownloadDecision> EpisodeSearch(Episode episode);
|
||||||
List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber);
|
List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +53,12 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
public List<DownloadDecision> EpisodeSearch(int episodeId)
|
public List<DownloadDecision> EpisodeSearch(int episodeId)
|
||||||
{
|
{
|
||||||
var episode = _episodeService.GetEpisode(episodeId);
|
var episode = _episodeService.GetEpisode(episodeId);
|
||||||
|
|
||||||
|
return EpisodeSearch(episode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DownloadDecision> EpisodeSearch(Episode episode)
|
||||||
|
{
|
||||||
var series = _seriesService.GetSeries(episode.SeriesId);
|
var series = _seriesService.GetSeries(episode.SeriesId);
|
||||||
|
|
||||||
if (series.SeriesType == SeriesTypes.Daily)
|
if (series.SeriesType == SeriesTypes.Daily)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
using NzbDrone.Common.Exceptions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Exceptions
|
||||||
|
{
|
||||||
|
public class RequestLimitReachedException : NzbDroneException
|
||||||
|
{
|
||||||
|
public RequestLimitReachedException(string message, params object[] args)
|
||||||
|
: base(message, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestLimitReachedException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,11 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
throw new ApiKeyException("Indexer requires an API key");
|
throw new ApiKeyException("Indexer requires an API key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorMessage == "Request limit reached")
|
||||||
|
{
|
||||||
|
throw new RequestLimitReachedException("API limit reached");
|
||||||
|
}
|
||||||
|
|
||||||
throw new NewznabException("Newznab error detected: {0}", errorMessage);
|
throw new NewznabException("Newznab error detected: {0}", errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,10 @@ namespace NzbDrone.Core.Indexers
|
||||||
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
|
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
|
||||||
throw new ValidationException(new List<ValidationFailure> {apiKeyFailure}.ToArray());
|
throw new ValidationException(new List<ValidationFailure> {apiKeyFailure}.ToArray());
|
||||||
}
|
}
|
||||||
|
catch (RequestLimitReachedException)
|
||||||
|
{
|
||||||
|
_logger.Warn("Request limit reached");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.WarnException("Indexer doesn't appear to be Newznab based: " + ex.Message, ex);
|
_logger.WarnException("Indexer doesn't appear to be Newznab based: " + ex.Message, ex);
|
||||||
|
|
|
@ -48,7 +48,6 @@ namespace NzbDrone.Core.Jobs
|
||||||
{
|
{
|
||||||
var defaultTasks = new[]
|
var defaultTasks = new[]
|
||||||
{
|
{
|
||||||
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
|
|
||||||
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
|
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
|
||||||
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
|
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
|
||||||
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
|
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
|
||||||
|
|
|
@ -4,6 +4,14 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
{
|
{
|
||||||
public class DownloadedEpisodesScanCommand : Command
|
public class DownloadedEpisodesScanCommand : Command
|
||||||
{
|
{
|
||||||
|
public override bool SendUpdatesToClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return SendUpdates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendUpdates { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,5 +13,14 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RescanSeriesCommand()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RescanSeriesCommand(int seriesId)
|
||||||
|
{
|
||||||
|
SeriesId = seriesId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System.IO;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
@ -16,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
{
|
{
|
||||||
public interface IDiskScanService
|
public interface IDiskScanService
|
||||||
{
|
{
|
||||||
|
void Scan(Series series);
|
||||||
string[] GetVideoFiles(string path, bool allDirectories = true);
|
string[] GetVideoFiles(string path, bool allDirectories = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Scan(Series series)
|
public void Scan(Series series)
|
||||||
{
|
{
|
||||||
_logger.ProgressInfo("Scanning disk for {0}", series.Title);
|
_logger.ProgressInfo("Scanning disk for {0}", series.Title);
|
||||||
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
|
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
|
||||||
|
@ -73,9 +75,16 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var videoFilesStopwatch = Stopwatch.StartNew();
|
||||||
var mediaFileList = GetVideoFiles(series.Path).ToList();
|
var mediaFileList = GetVideoFiles(series.Path).ToList();
|
||||||
|
videoFilesStopwatch.Stop();
|
||||||
|
_logger.Trace("Finished getting episode files for: {0} [{1}]", series, videoFilesStopwatch.Elapsed);
|
||||||
|
|
||||||
|
var decisionsStopwatch = Stopwatch.StartNew();
|
||||||
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false);
|
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false);
|
||||||
|
decisionsStopwatch.Stop();
|
||||||
|
_logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
|
||||||
|
|
||||||
_importApprovedEpisodes.Import(decisions);
|
_importApprovedEpisodes.Import(decisions);
|
||||||
|
|
||||||
_logger.Info("Completed scanning disk for {0}", series.Title);
|
_logger.Info("Completed scanning disk for {0}", series.Title);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common;
|
|
||||||
using NzbDrone.Common.Disk;
|
|
||||||
using NzbDrone.Core.MediaFiles;
|
|
||||||
using NzbDrone.Core.Metadata.Files;
|
|
||||||
using NzbDrone.Core.Tv;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Metadata.Consumers.Fake
|
|
||||||
{
|
|
||||||
public class FakeMetadata : MetadataBase<FakeMetadataSettings>
|
|
||||||
{
|
|
||||||
public FakeMetadata(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger)
|
|
||||||
: base(diskProvider, httpProvider, logger)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override MetadataFile FindMetadataFile(Series series, string path)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using System;
|
|
||||||
using FluentValidation;
|
|
||||||
using FluentValidation.Results;
|
|
||||||
using NzbDrone.Core.Annotations;
|
|
||||||
using NzbDrone.Core.ThingiProvider;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Metadata.Consumers.Fake
|
|
||||||
{
|
|
||||||
public class FakeMetadataSettingsValidator : AbstractValidator<FakeMetadataSettings>
|
|
||||||
{
|
|
||||||
public FakeMetadataSettingsValidator()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FakeMetadataSettings : IProviderConfig
|
|
||||||
{
|
|
||||||
private static readonly FakeMetadataSettingsValidator Validator = new FakeMetadataSettingsValidator();
|
|
||||||
|
|
||||||
public FakeMetadataSettings()
|
|
||||||
{
|
|
||||||
FakeSetting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[FieldDefinition(0, Label = "Fake Setting", Type = FieldType.Checkbox)]
|
|
||||||
public Boolean FakeSetting { get; set; }
|
|
||||||
|
|
||||||
public bool IsValid
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValidationResult Validate()
|
|
||||||
{
|
|
||||||
return Validator.Validate(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,468 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Remoting.Messaging;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.MediaCover;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Metadata.Files;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
|
||||||
|
{
|
||||||
|
public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings>
|
||||||
|
{
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly IMapCoversToLocal _mediaCoverService;
|
||||||
|
private readonly IMediaFileService _mediaFileService;
|
||||||
|
private readonly IMetadataFileService _metadataFileService;
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly IHttpProvider _httpProvider;
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public RoksboxMetadata(IEventAggregator eventAggregator,
|
||||||
|
IMapCoversToLocal mediaCoverService,
|
||||||
|
IMediaFileService mediaFileService,
|
||||||
|
IMetadataFileService metadataFileService,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IHttpProvider httpProvider,
|
||||||
|
IEpisodeService episodeService,
|
||||||
|
Logger logger)
|
||||||
|
: base(diskProvider, httpProvider, logger)
|
||||||
|
{
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_mediaCoverService = mediaCoverService;
|
||||||
|
_mediaFileService = mediaFileService;
|
||||||
|
_metadataFileService = metadataFileService;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_httpProvider = httpProvider;
|
||||||
|
_episodeService = episodeService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" };
|
||||||
|
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
||||||
|
{
|
||||||
|
var metadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
if (!_diskProvider.FolderExists(series.Path))
|
||||||
|
{
|
||||||
|
_logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SeriesImages)
|
||||||
|
{
|
||||||
|
var metadata = WriteSeriesImages(series, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SeasonImages)
|
||||||
|
{
|
||||||
|
var metadata = WriteSeasonImages(series, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.AddRange(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
if (Settings.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
if (Settings.EpisodeImages)
|
||||||
|
{
|
||||||
|
var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
|
||||||
|
|
||||||
|
if (metadataFile != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadataFiles.RemoveAll(c => c == null);
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
|
||||||
|
{
|
||||||
|
var metadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
if (Settings.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.EpisodeImages)
|
||||||
|
{
|
||||||
|
var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
|
||||||
|
|
||||||
|
if (metadataFile != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
||||||
|
{
|
||||||
|
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
|
||||||
|
var updatedMetadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
|
||||||
|
|
||||||
|
foreach (var metadataFile in metadataFiles)
|
||||||
|
{
|
||||||
|
string newFilename;
|
||||||
|
|
||||||
|
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||||
|
{
|
||||||
|
newFilename = GetEpisodeImageFilename(episodeFile.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
newFilename = GetEpisodeMetadataFilename(episodeFile.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
|
||||||
|
|
||||||
|
if (!newFilename.PathEquals(existingFilename))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(existingFilename, newFilename);
|
||||||
|
metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename);
|
||||||
|
|
||||||
|
updatedMetadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
|
if (filename == null) return null;
|
||||||
|
var parentdir = Directory.GetParent(path);
|
||||||
|
|
||||||
|
var metadata = new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
//Series and season images are both named folder.jpg, only season ones sit in season folders
|
||||||
|
if (String.Compare(filename, parentdir.Name, true) == 0)
|
||||||
|
{
|
||||||
|
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
|
||||||
|
if (seasonMatch.Success)
|
||||||
|
{
|
||||||
|
metadata.Type = MetadataType.SeasonImage;
|
||||||
|
|
||||||
|
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||||
|
|
||||||
|
if (seasonNumber.Contains("specials"))
|
||||||
|
{
|
||||||
|
metadata.SeasonNumber = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.Type = MetadataType.SeriesImage;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseResult = Parser.Parser.ParseTitle(filename);
|
||||||
|
|
||||||
|
if (parseResult != null &&
|
||||||
|
!parseResult.FullSeason)
|
||||||
|
{
|
||||||
|
switch (Path.GetExtension(filename).ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case ".xml":
|
||||||
|
metadata.Type = MetadataType.EpisodeMetadata;
|
||||||
|
return metadata;
|
||||||
|
case ".jpg":
|
||||||
|
metadata.Type = MetadataType.EpisodeImage;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first
|
||||||
|
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||||
|
var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
|
||||||
|
|
||||||
|
//TODO: Do we want to overwrite the file if it exists?
|
||||||
|
if (_diskProvider.FileExists(destination))
|
||||||
|
{
|
||||||
|
_logger.Debug("Series image: {0} already exists.", image.CoverType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
_diskProvider.CopyFile(source, destination, false);
|
||||||
|
|
||||||
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.SeriesImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
_logger.Debug("Writing season images for {0}.", series.Title);
|
||||||
|
//Create a dictionary between season number and output folder
|
||||||
|
var seasonFolderMap = new Dictionary<int, string>();
|
||||||
|
foreach (var folder in Directory.EnumerateDirectories(series.Path))
|
||||||
|
{
|
||||||
|
var directoryinfo = new DirectoryInfo(folder);
|
||||||
|
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
|
||||||
|
if (seasonMatch.Success)
|
||||||
|
{
|
||||||
|
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||||
|
|
||||||
|
if (seasonNumber.Contains("specials"))
|
||||||
|
{
|
||||||
|
seasonFolderMap[0] = folder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int matchedSeason;
|
||||||
|
if (Int32.TryParse(seasonNumber, out matchedSeason))
|
||||||
|
{
|
||||||
|
seasonFolderMap[matchedSeason] = folder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var season in series.Seasons)
|
||||||
|
{
|
||||||
|
//Work out the path to this season - if we don't have a matching path then skip this season.
|
||||||
|
string seasonFolder;
|
||||||
|
if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
|
||||||
|
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var filename = Path.GetFileName(seasonFolder) + ".jpg";
|
||||||
|
|
||||||
|
var path = Path.Combine(series.Path, seasonFolder, filename);
|
||||||
|
_logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
|
||||||
|
DownloadImage(series, image.Url, path);
|
||||||
|
|
||||||
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
|
||||||
|
c.SeasonNumber == season.SeasonNumber) ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
SeasonNumber = season.SeasonNumber,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.SeasonImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
var filename = GetEpisodeMetadataFilename(episodeFile.Path);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
||||||
|
|
||||||
|
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
|
||||||
|
c.EpisodeFileId == episodeFile.Id);
|
||||||
|
|
||||||
|
if (existingMetadata != null)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||||
|
if (!filename.PathEquals(fullPath))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(fullPath, filename);
|
||||||
|
existingMetadata.RelativePath = relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
|
||||||
|
|
||||||
|
var xmlResult = String.Empty;
|
||||||
|
foreach (var episode in episodeFile.Episodes.Value)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var xws = new XmlWriterSettings();
|
||||||
|
xws.OmitXmlDeclaration = true;
|
||||||
|
xws.Indent = false;
|
||||||
|
|
||||||
|
using (var xw = XmlWriter.Create(sb, xws))
|
||||||
|
{
|
||||||
|
var doc = new XDocument();
|
||||||
|
|
||||||
|
var details = new XElement("video");
|
||||||
|
details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
|
||||||
|
details.Add(new XElement("year", episode.AirDate));
|
||||||
|
details.Add(new XElement("genre", String.Join(" / ", series.Genres)));
|
||||||
|
var actors = String.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count)));
|
||||||
|
details.Add(new XElement("actors", actors));
|
||||||
|
details.Add(new XElement("description", episode.Overview));
|
||||||
|
details.Add(new XElement("length", series.Runtime));
|
||||||
|
details.Add(new XElement("mpaa", ValidCertification.Contains( series.Certification.ToUpperInvariant() ) ? series.Certification.ToUpperInvariant() : "UNRATED" ) );
|
||||||
|
doc.Add(details);
|
||||||
|
doc.Save(xw);
|
||||||
|
|
||||||
|
xmlResult += doc.ToString();
|
||||||
|
xmlResult += Environment.NewLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Saving episodedetails to: {0}", filename);
|
||||||
|
_diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
|
||||||
|
|
||||||
|
var metadata = existingMetadata ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
EpisodeFileId = episodeFile.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.EpisodeMetadata,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||||
|
|
||||||
|
if (screenshot == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Episode screenshot not available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = GetEpisodeImageFilename(episodeFile.Path);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
||||||
|
|
||||||
|
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
|
||||||
|
c.EpisodeFileId == episodeFile.Id);
|
||||||
|
|
||||||
|
if (existingMetadata != null)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||||
|
if (!filename.PathEquals(fullPath))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(fullPath, filename);
|
||||||
|
existingMetadata.RelativePath = relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadImage(series, screenshot.Url, filename);
|
||||||
|
|
||||||
|
var metadata = existingMetadata ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
EpisodeFileId = episodeFile.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.EpisodeImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetEpisodeMetadataFilename(string episodeFilePath)
|
||||||
|
{
|
||||||
|
return Path.ChangeExtension(episodeFilePath, "xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||||
|
{
|
||||||
|
return Path.ChangeExtension(episodeFilePath, "jpg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
|
||||||
|
{
|
||||||
|
public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings>
|
||||||
|
{
|
||||||
|
public RoksboxSettingsValidator()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RoksboxMetadataSettings : IProviderConfig
|
||||||
|
{
|
||||||
|
private static readonly RoksboxSettingsValidator Validator = new RoksboxSettingsValidator();
|
||||||
|
|
||||||
|
public RoksboxMetadataSettings()
|
||||||
|
{
|
||||||
|
EpisodeMetadata = true;
|
||||||
|
SeriesImages = true;
|
||||||
|
SeasonImages = true;
|
||||||
|
EpisodeImages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean EpisodeMetadata { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean SeriesImages { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean SeasonImages { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean EpisodeImages { get; set; }
|
||||||
|
|
||||||
|
public bool IsValid
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResult Validate()
|
||||||
|
{
|
||||||
|
return Validator.Validate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,475 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Remoting.Messaging;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.MediaCover;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Metadata.Files;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
|
||||||
|
{
|
||||||
|
public class WdtvMetadata : MetadataBase<WdtvMetadataSettings>
|
||||||
|
{
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly IMapCoversToLocal _mediaCoverService;
|
||||||
|
private readonly IMediaFileService _mediaFileService;
|
||||||
|
private readonly IMetadataFileService _metadataFileService;
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly IHttpProvider _httpProvider;
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public WdtvMetadata(IEventAggregator eventAggregator,
|
||||||
|
IMapCoversToLocal mediaCoverService,
|
||||||
|
IMediaFileService mediaFileService,
|
||||||
|
IMetadataFileService metadataFileService,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IHttpProvider httpProvider,
|
||||||
|
IEpisodeService episodeService,
|
||||||
|
Logger logger)
|
||||||
|
: base(diskProvider, httpProvider, logger)
|
||||||
|
{
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
|
_mediaCoverService = mediaCoverService;
|
||||||
|
_mediaFileService = mediaFileService;
|
||||||
|
_metadataFileService = metadataFileService;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_httpProvider = httpProvider;
|
||||||
|
_episodeService = episodeService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public override void OnSeriesUpdated(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
||||||
|
{
|
||||||
|
var metadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
if (!_diskProvider.FolderExists(series.Path))
|
||||||
|
{
|
||||||
|
_logger.Info("Series folder ({0}) does not exist, skipping metadata creation", series.Path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SeriesImages)
|
||||||
|
{
|
||||||
|
var metadata = WriteSeriesImages(series, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SeasonImages)
|
||||||
|
{
|
||||||
|
var metadata = WriteSeasonImages(series, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.AddRange(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
if (Settings.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
var metadata = WriteEpisodeMetadata(series, episodeFile, existingMetadataFiles);
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
if (Settings.EpisodeImages)
|
||||||
|
{
|
||||||
|
var metadataFile = WriteEpisodeImages(series, episodeFile, existingMetadataFiles);
|
||||||
|
|
||||||
|
if (metadataFile != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadataFiles.RemoveAll(c => c == null);
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnEpisodeImport(Series series, EpisodeFile episodeFile, bool newDownload)
|
||||||
|
{
|
||||||
|
var metadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
if (Settings.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(WriteEpisodeMetadata(series, episodeFile, new List<MetadataFile>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.EpisodeImages)
|
||||||
|
{
|
||||||
|
var metadataFile = WriteEpisodeImages(series, episodeFile, new List<MetadataFile>());
|
||||||
|
|
||||||
|
if (metadataFile != null)
|
||||||
|
{
|
||||||
|
metadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(metadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
|
||||||
|
{
|
||||||
|
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
|
||||||
|
var updatedMetadataFiles = new List<MetadataFile>();
|
||||||
|
|
||||||
|
foreach (var episodeFile in episodeFiles)
|
||||||
|
{
|
||||||
|
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
|
||||||
|
|
||||||
|
foreach (var metadataFile in metadataFiles)
|
||||||
|
{
|
||||||
|
string newFilename;
|
||||||
|
|
||||||
|
if (metadataFile.Type == MetadataType.EpisodeImage)
|
||||||
|
{
|
||||||
|
newFilename = GetEpisodeImageFilename(episodeFile.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
|
||||||
|
{
|
||||||
|
newFilename = GetEpisodeMetadataFilename(episodeFile.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
|
||||||
|
|
||||||
|
if (!newFilename.PathEquals(existingFilename))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(existingFilename, newFilename);
|
||||||
|
metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename);
|
||||||
|
|
||||||
|
updatedMetadataFiles.Add(metadataFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override MetadataFile FindMetadataFile(Series series, string path)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
|
if (filename == null) return null;
|
||||||
|
|
||||||
|
var metadata = new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
//Series and season images are both named folder.jpg, only season ones sit in season folders
|
||||||
|
if (String.Compare(filename, "folder.jpg", true) == 0)
|
||||||
|
{
|
||||||
|
var parentdir = Directory.GetParent(path);
|
||||||
|
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
|
||||||
|
if (seasonMatch.Success)
|
||||||
|
{
|
||||||
|
metadata.Type = MetadataType.SeasonImage;
|
||||||
|
|
||||||
|
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||||
|
|
||||||
|
if (seasonNumber.Contains("specials"))
|
||||||
|
{
|
||||||
|
metadata.SeasonNumber = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.SeasonNumber = Convert.ToInt32(seasonNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
metadata.Type = MetadataType.SeriesImage;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseResult = Parser.Parser.ParseTitle(filename);
|
||||||
|
|
||||||
|
if (parseResult != null &&
|
||||||
|
!parseResult.FullSeason)
|
||||||
|
{
|
||||||
|
switch (Path.GetExtension(filename).ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case ".xml":
|
||||||
|
metadata.Type = MetadataType.EpisodeMetadata;
|
||||||
|
return metadata;
|
||||||
|
case ".metathumb":
|
||||||
|
metadata.Type = MetadataType.EpisodeImage;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteSeriesImages(Series series, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first
|
||||||
|
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
|
||||||
|
var destination = Path.Combine(series.Path, "folder" + Path.GetExtension(source));
|
||||||
|
|
||||||
|
//TODO: Do we want to overwrite the file if it exists?
|
||||||
|
if (_diskProvider.FileExists(destination))
|
||||||
|
{
|
||||||
|
_logger.Debug("Series image: {0} already exists.", image.CoverType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
_diskProvider.CopyFile(source, destination, false);
|
||||||
|
|
||||||
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage) ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.SeriesImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, destination)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MetadataFile> WriteSeasonImages(Series series, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
_logger.Debug("Writing season images for {0}.", series.Title);
|
||||||
|
//Create a dictionary between season number and output folder
|
||||||
|
var seasonFolderMap = new Dictionary<int, string>();
|
||||||
|
foreach (var folder in Directory.EnumerateDirectories(series.Path))
|
||||||
|
{
|
||||||
|
var directoryinfo = new DirectoryInfo(folder);
|
||||||
|
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
|
||||||
|
if (seasonMatch.Success)
|
||||||
|
{
|
||||||
|
var seasonNumber = seasonMatch.Groups["season"].Value;
|
||||||
|
|
||||||
|
if (seasonNumber.Contains("specials"))
|
||||||
|
{
|
||||||
|
seasonFolderMap[0] = folder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int matchedSeason;
|
||||||
|
if (Int32.TryParse(seasonNumber, out matchedSeason))
|
||||||
|
{
|
||||||
|
seasonFolderMap[matchedSeason] = folder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var season in series.Seasons)
|
||||||
|
{
|
||||||
|
//Work out the path to this season - if we don't have a matching path then skip this season.
|
||||||
|
string seasonFolder;
|
||||||
|
if (!seasonFolderMap.TryGetValue(season.SeasonNumber, out seasonFolder))
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
|
||||||
|
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var filename = "folder.jpg";
|
||||||
|
|
||||||
|
var path = Path.Combine(series.Path, seasonFolder, filename);
|
||||||
|
_logger.Debug("Writing season image for series {0}, season {1} to {2}.", series.Title, season.SeasonNumber, path);
|
||||||
|
DownloadImage(series, image.Url, path);
|
||||||
|
|
||||||
|
var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage &&
|
||||||
|
c.SeasonNumber == season.SeasonNumber) ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
SeasonNumber = season.SeasonNumber,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.SeasonImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteEpisodeMetadata(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
var filename = GetEpisodeMetadataFilename(episodeFile.Path);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
||||||
|
|
||||||
|
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata &&
|
||||||
|
c.EpisodeFileId == episodeFile.Id);
|
||||||
|
|
||||||
|
if (existingMetadata != null)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||||
|
if (!filename.PathEquals(fullPath))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(fullPath, filename);
|
||||||
|
existingMetadata.RelativePath = relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Generating {0} for: {1}", filename, episodeFile.Path);
|
||||||
|
|
||||||
|
var xmlResult = String.Empty;
|
||||||
|
foreach (var episode in episodeFile.Episodes.Value)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var xws = new XmlWriterSettings();
|
||||||
|
xws.OmitXmlDeclaration = true;
|
||||||
|
xws.Indent = false;
|
||||||
|
|
||||||
|
using (var xw = XmlWriter.Create(sb, xws))
|
||||||
|
{
|
||||||
|
var doc = new XDocument();
|
||||||
|
|
||||||
|
var details = new XElement("details");
|
||||||
|
details.Add(new XElement("id", series.Id));
|
||||||
|
details.Add(new XElement("title", String.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title)));
|
||||||
|
details.Add(new XElement("series_name", series.Title));
|
||||||
|
details.Add(new XElement("episode_name", episode.Title));
|
||||||
|
details.Add(new XElement("season_number", episode.SeasonNumber));
|
||||||
|
details.Add(new XElement("episode_number", episode.EpisodeNumber));
|
||||||
|
details.Add(new XElement("firstaired", episode.AirDate));
|
||||||
|
details.Add(new XElement("genre", String.Join(" / ", series.Genres)));
|
||||||
|
details.Add(new XElement("actor", String.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character))));
|
||||||
|
details.Add(new XElement("overview", episode.Overview));
|
||||||
|
|
||||||
|
|
||||||
|
//Todo: get guest stars, writer and director
|
||||||
|
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
|
||||||
|
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
|
||||||
|
|
||||||
|
doc.Add(details);
|
||||||
|
doc.Save(xw);
|
||||||
|
|
||||||
|
xmlResult += doc.ToString();
|
||||||
|
xmlResult += Environment.NewLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Saving episodedetails to: {0}", filename);
|
||||||
|
_diskProvider.WriteAllText(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
|
||||||
|
|
||||||
|
var metadata = existingMetadata ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
EpisodeFileId = episodeFile.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.EpisodeMetadata,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MetadataFile WriteEpisodeImages(Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
|
||||||
|
{
|
||||||
|
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
|
||||||
|
|
||||||
|
if (screenshot == null)
|
||||||
|
{
|
||||||
|
_logger.Trace("Episode screenshot not available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = GetEpisodeImageFilename(episodeFile.Path);
|
||||||
|
var relativePath = DiskProviderBase.GetRelativePath(series.Path, filename);
|
||||||
|
|
||||||
|
var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeImage &&
|
||||||
|
c.EpisodeFileId == episodeFile.Id);
|
||||||
|
|
||||||
|
if (existingMetadata != null)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
|
||||||
|
if (!filename.PathEquals(fullPath))
|
||||||
|
{
|
||||||
|
_diskProvider.MoveFile(fullPath, filename);
|
||||||
|
existingMetadata.RelativePath = relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadImage(series, screenshot.Url, filename);
|
||||||
|
|
||||||
|
var metadata = existingMetadata ??
|
||||||
|
new MetadataFile
|
||||||
|
{
|
||||||
|
SeriesId = series.Id,
|
||||||
|
EpisodeFileId = episodeFile.Id,
|
||||||
|
Consumer = GetType().Name,
|
||||||
|
Type = MetadataType.EpisodeImage,
|
||||||
|
RelativePath = DiskProviderBase.GetRelativePath(series.Path, filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetEpisodeMetadataFilename(string episodeFilePath)
|
||||||
|
{
|
||||||
|
return Path.ChangeExtension(episodeFilePath, "xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetEpisodeImageFilename(string episodeFilePath)
|
||||||
|
{
|
||||||
|
return Path.ChangeExtension(episodeFilePath, "metathumb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
|
||||||
|
{
|
||||||
|
public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings>
|
||||||
|
{
|
||||||
|
public WdtvSettingsValidator()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WdtvMetadataSettings : IProviderConfig
|
||||||
|
{
|
||||||
|
private static readonly WdtvSettingsValidator Validator = new WdtvSettingsValidator();
|
||||||
|
|
||||||
|
public WdtvMetadataSettings()
|
||||||
|
{
|
||||||
|
EpisodeMetadata = true;
|
||||||
|
SeriesImages = true;
|
||||||
|
SeasonImages = true;
|
||||||
|
EpisodeImages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean EpisodeMetadata { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean SeriesImages { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean SeasonImages { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
|
||||||
|
public Boolean EpisodeImages { get; set; }
|
||||||
|
|
||||||
|
public bool IsValid
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResult Validate()
|
||||||
|
{
|
||||||
|
return Validator.Validate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Remoting.Messaging;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
@ -10,7 +9,6 @@ using System.Xml.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Core.Datastore;
|
|
||||||
using NzbDrone.Core.MediaCover;
|
using NzbDrone.Core.MediaCover;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Composition;
|
using NzbDrone.Common.Composition;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Metadata.Consumers.Fake;
|
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Metadata
|
namespace NzbDrone.Core.Metadata
|
||||||
|
@ -30,8 +29,6 @@ namespace NzbDrone.Core.Metadata
|
||||||
|
|
||||||
foreach (var provider in _providers)
|
foreach (var provider in _providers)
|
||||||
{
|
{
|
||||||
if (provider.GetType() == typeof(FakeMetadata)) continue;;
|
|
||||||
|
|
||||||
definitions.Add(new MetadataDefinition
|
definitions.Add(new MetadataDefinition
|
||||||
{
|
{
|
||||||
Enable = false,
|
Enable = false,
|
||||||
|
|
|
@ -236,6 +236,8 @@
|
||||||
<Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" />
|
<Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" />
|
||||||
<Compile Include="Download\Clients\DownloadClientException.cs" />
|
<Compile Include="Download\Clients\DownloadClientException.cs" />
|
||||||
<Compile Include="Download\Clients\FolderSettings.cs" />
|
<Compile Include="Download\Clients\FolderSettings.cs" />
|
||||||
|
<Compile Include="Download\Clients\Nzbget\NzbgetHistoryItem.cs" />
|
||||||
|
<Compile Include="Download\Clients\Nzbget\NzbgetParameter.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\TestNzbgetCommand.cs" />
|
<Compile Include="Download\Clients\Nzbget\TestNzbgetCommand.cs" />
|
||||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||||
|
@ -280,12 +282,14 @@
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
|
||||||
|
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
|
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
|
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
|
||||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
|
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
|
||||||
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
||||||
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
||||||
<Compile Include="Housekeeping\IHousekeepingTask.cs" />
|
<Compile Include="Housekeeping\IHousekeepingTask.cs" />
|
||||||
|
<Compile Include="IndexerSearch\MissingEpisodeSearchCommand.cs" />
|
||||||
<Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
|
<Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
|
||||||
<Compile Include="IndexerSearch\SeriesSearchService.cs" />
|
<Compile Include="IndexerSearch\SeriesSearchService.cs" />
|
||||||
<Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
|
<Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
|
||||||
|
@ -296,6 +300,7 @@
|
||||||
<Compile Include="Indexers\BasicTorrentRssParser.cs" />
|
<Compile Include="Indexers\BasicTorrentRssParser.cs" />
|
||||||
<Compile Include="Indexers\DownloadProtocols.cs" />
|
<Compile Include="Indexers\DownloadProtocols.cs" />
|
||||||
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
|
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
|
||||||
|
<Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" />
|
||||||
<Compile Include="Indexers\Eztv\Eztv.cs" />
|
<Compile Include="Indexers\Eztv\Eztv.cs" />
|
||||||
<Compile Include="Indexers\FetchAndParseRssService.cs" />
|
<Compile Include="Indexers\FetchAndParseRssService.cs" />
|
||||||
<Compile Include="Indexers\IIndexer.cs" />
|
<Compile Include="Indexers\IIndexer.cs" />
|
||||||
|
@ -341,9 +346,11 @@
|
||||||
<Compile Include="MetadataSource\Trakt\Actor.cs" />
|
<Compile Include="MetadataSource\Trakt\Actor.cs" />
|
||||||
<Compile Include="MetadataSource\Trakt\People.cs" />
|
<Compile Include="MetadataSource\Trakt\People.cs" />
|
||||||
<Compile Include="MetadataSource\Trakt\Ratings.cs" />
|
<Compile Include="MetadataSource\Trakt\Ratings.cs" />
|
||||||
|
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
|
||||||
|
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
|
||||||
|
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
|
||||||
|
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" />
|
||||||
<Compile Include="Metadata\Files\CleanMetadataService.cs" />
|
<Compile Include="Metadata\Files\CleanMetadataService.cs" />
|
||||||
<Compile Include="Metadata\Consumers\Fake\Fake.cs" />
|
|
||||||
<Compile Include="Metadata\Consumers\Fake\FakeSettings.cs" />
|
|
||||||
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
|
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
|
||||||
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
|
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
|
||||||
<Compile Include="Metadata\ExistingMetadataService.cs" />
|
<Compile Include="Metadata\ExistingMetadataService.cs" />
|
||||||
|
@ -498,10 +505,10 @@
|
||||||
<Compile Include="Instrumentation\LogService.cs" />
|
<Compile Include="Instrumentation\LogService.cs" />
|
||||||
<Compile Include="Instrumentation\DatabaseTarget.cs" />
|
<Compile Include="Instrumentation\DatabaseTarget.cs" />
|
||||||
<Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" />
|
<Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\EnqueueResponse.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetBooleanResponse.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\ErrorModel.cs" />
|
<Compile Include="Download\Clients\Nzbget\ErrorModel.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\JsonError.cs" />
|
<Compile Include="Download\Clients\Nzbget\JsonError.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetQueue.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetListResponse.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetPriority.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetPriority.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\VersionResponse.cs" />
|
<Compile Include="Download\Clients\Nzbget\VersionResponse.cs" />
|
||||||
|
@ -663,6 +670,7 @@
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Tv\SeriesStatusType.cs" />
|
<Compile Include="Tv\SeriesStatusType.cs" />
|
||||||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||||
|
<Compile Include="Tv\ShouldRefreshSeries.cs" />
|
||||||
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
|
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
|
||||||
<Compile Include="Update\Commands\InstallUpdateCommand.cs" />
|
<Compile Include="Update\Commands\InstallUpdateCommand.cs" />
|
||||||
<Compile Include="Update\InstallUpdateService.cs" />
|
<Compile Include="Update\InstallUpdateService.cs" />
|
||||||
|
|
|
@ -102,7 +102,7 @@ namespace NzbDrone.Core.Parser
|
||||||
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_",
|
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^)(a|an|the|and|or|of)(?:\b|_))|\W|_",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[x|h|x\s|h\s]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|\<|\>|\?|\*|\:|\|",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);
|
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);
|
||||||
|
|
|
@ -31,7 +31,17 @@ namespace NzbDrone.Core.RootFolders
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private static readonly HashSet<string> SpecialFolders = new HashSet<string> { "$recycle.bin", "system volume information", "recycler", "lost+found" };
|
private static readonly HashSet<string> SpecialFolders = new HashSet<string>
|
||||||
|
{
|
||||||
|
"$recycle.bin",
|
||||||
|
"system volume information",
|
||||||
|
"recycler",
|
||||||
|
"lost+found",
|
||||||
|
".appledb",
|
||||||
|
".appledesktop",
|
||||||
|
".appledouble",
|
||||||
|
"@eadir"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
public RootFolderService(IRootFolderRepository rootFolderRepository,
|
public RootFolderService(IRootFolderRepository rootFolderRepository,
|
||||||
|
@ -123,11 +133,8 @@ namespace NzbDrone.Core.RootFolders
|
||||||
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
|
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Path.GetPathRoot(path).Equals(path, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
var setToRemove = SpecialFolders;
|
var setToRemove = SpecialFolders;
|
||||||
results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name));
|
results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name));
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Debug("{0} unmapped folders detected.", results.Count);
|
Logger.Debug("{0} unmapped folders detected.", results.Count);
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -5,6 +5,8 @@ using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Core.DataAugmentation.DailySeries;
|
using NzbDrone.Core.DataAugmentation.DailySeries;
|
||||||
using NzbDrone.Core.Instrumentation.Extensions;
|
using NzbDrone.Core.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
using NzbDrone.Core.MediaFiles.Commands;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.MetadataSource;
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
@ -21,15 +23,26 @@ namespace NzbDrone.Core.Tv
|
||||||
private readonly IRefreshEpisodeService _refreshEpisodeService;
|
private readonly IRefreshEpisodeService _refreshEpisodeService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly IDailySeriesService _dailySeriesService;
|
private readonly IDailySeriesService _dailySeriesService;
|
||||||
|
private readonly IDiskScanService _diskScanService;
|
||||||
|
private readonly ICheckIfSeriesShouldBeRefreshed _checkIfSeriesShouldBeRefreshed;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public RefreshSeriesService(IProvideSeriesInfo seriesInfo, ISeriesService seriesService, IRefreshEpisodeService refreshEpisodeService, IEventAggregator eventAggregator, IDailySeriesService dailySeriesService, Logger logger)
|
public RefreshSeriesService(IProvideSeriesInfo seriesInfo,
|
||||||
|
ISeriesService seriesService,
|
||||||
|
IRefreshEpisodeService refreshEpisodeService,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
IDailySeriesService dailySeriesService,
|
||||||
|
IDiskScanService diskScanService,
|
||||||
|
ICheckIfSeriesShouldBeRefreshed checkIfSeriesShouldBeRefreshed,
|
||||||
|
Logger logger)
|
||||||
{
|
{
|
||||||
_seriesInfo = seriesInfo;
|
_seriesInfo = seriesInfo;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
_refreshEpisodeService = refreshEpisodeService;
|
_refreshEpisodeService = refreshEpisodeService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_dailySeriesService = dailySeriesService;
|
_dailySeriesService = dailySeriesService;
|
||||||
|
_diskScanService = diskScanService;
|
||||||
|
_checkIfSeriesShouldBeRefreshed = checkIfSeriesShouldBeRefreshed;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +128,8 @@ namespace NzbDrone.Core.Tv
|
||||||
var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.Title).ToList();
|
var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.Title).ToList();
|
||||||
|
|
||||||
foreach (var series in allSeries)
|
foreach (var series in allSeries)
|
||||||
|
{
|
||||||
|
if (_checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -125,6 +140,20 @@ namespace NzbDrone.Core.Tv
|
||||||
_logger.ErrorException("Couldn't refresh info for {0}".Inject(series), e);
|
_logger.ErrorException("Couldn't refresh info for {0}".Inject(series), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Info("Skipping refresh of series: {0}", series.Title);
|
||||||
|
_diskScanService.Scan(series);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.ErrorException("Couldn't rescan series {0}".Inject(series), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Tv
|
||||||
|
{
|
||||||
|
public interface ICheckIfSeriesShouldBeRefreshed
|
||||||
|
{
|
||||||
|
bool ShouldRefresh(Series series);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ShouldRefreshSeries : ICheckIfSeriesShouldBeRefreshed
|
||||||
|
{
|
||||||
|
private readonly IEpisodeService _episodeService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public ShouldRefreshSeries(IEpisodeService episodeService, Logger logger)
|
||||||
|
{
|
||||||
|
_episodeService = episodeService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldRefresh(Series series)
|
||||||
|
{
|
||||||
|
if (series.Status == SeriesStatusType.Continuing)
|
||||||
|
{
|
||||||
|
_logger.Trace("Series {0} is continuing, should refresh.", series.Title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.LastInfoSync < DateTime.UtcNow.AddDays(-30))
|
||||||
|
{
|
||||||
|
_logger.Trace("Series {0} last updated more than 30 days ago, should refresh.", series.Title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastEpisode = _episodeService.GetEpisodeBySeries(series.Id).OrderByDescending(e => e.AirDateUtc).FirstOrDefault();
|
||||||
|
|
||||||
|
if (lastEpisode != null && lastEpisode.AirDateUtc > DateTime.UtcNow.AddDays(-30))
|
||||||
|
{
|
||||||
|
_logger.Trace("Last episode in {0} aired less than 30 days ago, should refresh.", series.Title);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Series {0} should not be refreshed.", series.Title);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,12 @@ namespace NzbDrone.Host.AccessControl
|
||||||
var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort);
|
var localHttpsUrls = BuildUrls("https", "localhost", _configFileProvider.SslPort);
|
||||||
var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort);
|
var wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort);
|
||||||
|
|
||||||
|
if (!_configFileProvider.EnableSsl)
|
||||||
|
{
|
||||||
|
localHttpsUrls.Clear();
|
||||||
|
wildcardHttpsUrls.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
|
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
|
||||||
{
|
{
|
||||||
var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls;
|
var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls;
|
||||||
|
@ -51,7 +57,6 @@ namespace NzbDrone.Host.AccessControl
|
||||||
Urls.AddRange(httpUrls);
|
Urls.AddRange(httpUrls);
|
||||||
Urls.AddRange(httpsUrls);
|
Urls.AddRange(httpsUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Urls.AddRange(wildcardHttpUrls);
|
Urls.AddRange(wildcardHttpUrls);
|
||||||
|
|
|
@ -148,6 +148,9 @@
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -76,7 +76,9 @@
|
||||||
<Name>NzbDrone.Test.Common</Name>
|
<Name>NzbDrone.Test.Common</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -100,6 +100,9 @@
|
||||||
<Name>NzbDrone.Test.Common</Name>
|
<Name>NzbDrone.Test.Common</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -87,6 +87,9 @@
|
||||||
<Name>NzbDrone.Update</Name>
|
<Name>NzbDrone.Update</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -101,6 +101,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
|
|
@ -8,16 +8,16 @@
|
||||||
<option es3="false" />
|
<option es3="false" />
|
||||||
<option forin="true" />
|
<option forin="true" />
|
||||||
<option immed="true" />
|
<option immed="true" />
|
||||||
<option latedef="true" />
|
|
||||||
<option newcap="true" />
|
<option newcap="true" />
|
||||||
<option noarg="true" />
|
<option noarg="true" />
|
||||||
<option noempty="false" />
|
<option noempty="false" />
|
||||||
<option nonew="true" />
|
<option nonew="true" />
|
||||||
<option plusplus="false" />
|
<option plusplus="false" />
|
||||||
<option undef="true" />
|
<option undef="true" />
|
||||||
<option unused="true" />
|
|
||||||
<option strict="true" />
|
<option strict="true" />
|
||||||
<option trailing="false" />
|
<option trailing="false" />
|
||||||
|
<option latedef="true" />
|
||||||
|
<option unused="true" />
|
||||||
<option quotmark="single" />
|
<option quotmark="single" />
|
||||||
<option maxdepth="3" />
|
<option maxdepth="3" />
|
||||||
<option asi="false" />
|
<option asi="false" />
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
define(
|
||||||
|
[
|
||||||
|
'marionette',
|
||||||
|
'System/StatusModel',
|
||||||
|
'Mixins/CopyToClipboard'
|
||||||
|
], function (Marionette, StatusModel) {
|
||||||
|
return Marionette.Layout.extend({
|
||||||
|
template: 'Calendar/CalendarFeedViewTemplate',
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
icalUrl : '.x-ical-url',
|
||||||
|
icalCopy : '.x-ical-copy'
|
||||||
|
},
|
||||||
|
|
||||||
|
templateHelpers: {
|
||||||
|
icalHttpUrl : window.location.protocol + '//' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics',
|
||||||
|
icalWebCalUrl : 'webcal://' + window.location.host + StatusModel.get('urlBase') + '/feed/calendar/NzbDrone.ics'
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow: function () {
|
||||||
|
this.ui.icalCopy.copyToClipboard(this.ui.icalUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h3>NzbDrone Calendar feed</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body edit-series-modal">
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label">iCal feed</label>
|
||||||
|
|
||||||
|
<div class="controls ical-url">
|
||||||
|
<div class="input-append">
|
||||||
|
<input type="text" class="x-ical-url" value="{{icalHttpUrl}}" readonly="readonly" />
|
||||||
|
<button class="btn btn-icon-only x-ical-copy" title="Copy to clipboard"><i class="icon-copy"></i></button>
|
||||||
|
<a class="btn btn-icon-only no-router" title="Subscribe" href="{{icalWebCalUrl}}" target="_blank"><i class="icon-calendar-empty"></i></a>
|
||||||
|
</div>
|
||||||
|
<span class="help-inline">
|
||||||
|
<i class="icon-nd-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" data-dismiss="modal">close</button>
|
||||||
|
</div>
|
|
@ -1,10 +1,12 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
|
'AppLayout',
|
||||||
'marionette',
|
'marionette',
|
||||||
'Calendar/UpcomingCollectionView',
|
'Calendar/UpcomingCollectionView',
|
||||||
'Calendar/CalendarView'
|
'Calendar/CalendarView',
|
||||||
], function (Marionette, UpcomingCollectionView, CalendarView) {
|
'Calendar/CalendarFeedView'
|
||||||
|
], function (AppLayout, Marionette, UpcomingCollectionView, CalendarView, CalendarFeedView) {
|
||||||
return Marionette.Layout.extend({
|
return Marionette.Layout.extend({
|
||||||
template: 'Calendar/CalendarLayoutTemplate',
|
template: 'Calendar/CalendarLayoutTemplate',
|
||||||
|
|
||||||
|
@ -13,6 +15,10 @@ define(
|
||||||
calendar: '#x-calendar'
|
calendar: '#x-calendar'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
events: {
|
||||||
|
'click .x-ical': '_showiCal'
|
||||||
|
},
|
||||||
|
|
||||||
onShow: function () {
|
onShow: function () {
|
||||||
this._showUpcoming();
|
this._showUpcoming();
|
||||||
this._showCalendar();
|
this._showCalendar();
|
||||||
|
@ -24,6 +30,11 @@ define(
|
||||||
|
|
||||||
_showCalendar: function () {
|
_showCalendar: function () {
|
||||||
this.calendar.show(new CalendarView());
|
this.calendar.show(new CalendarView());
|
||||||
|
},
|
||||||
|
|
||||||
|
_showiCal: function () {
|
||||||
|
var view = new CalendarFeedView();
|
||||||
|
AppLayout.modalRegion.show(view);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="span3">
|
<div class="span3">
|
||||||
|
<div class="pull-left">
|
||||||
<h4>Upcoming</h4>
|
<h4>Upcoming</h4>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<h4>
|
||||||
|
<i class="icon-calendar-empty ical x-ical"></i>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<div id="x-upcoming"/>
|
<div id="x-upcoming"/>
|
||||||
</div>
|
</div>
|
||||||
<div class=span9>
|
<div class=span9>
|
||||||
|
|
|
@ -8,25 +8,24 @@ define(
|
||||||
'Calendar/Collection',
|
'Calendar/Collection',
|
||||||
'System/StatusModel',
|
'System/StatusModel',
|
||||||
'History/Queue/QueueCollection',
|
'History/Queue/QueueCollection',
|
||||||
|
'Config',
|
||||||
'Mixins/backbone.signalr.mixin',
|
'Mixins/backbone.signalr.mixin',
|
||||||
'fullcalendar',
|
'fullcalendar',
|
||||||
'jquery.easypiechart'
|
'jquery.easypiechart'
|
||||||
], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) {
|
], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) {
|
||||||
|
|
||||||
var _instance;
|
|
||||||
|
|
||||||
return Marionette.ItemView.extend({
|
return Marionette.ItemView.extend({
|
||||||
|
storageKey: 'calendar.view',
|
||||||
|
|
||||||
initialize: function () {
|
initialize: function () {
|
||||||
this.collection = new CalendarCollection().bindSignalR({ updateOnly: true });
|
this.collection = new CalendarCollection().bindSignalR({ updateOnly: true });
|
||||||
this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
|
this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
|
||||||
this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents);
|
this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents);
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function () {
|
render : function () {
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.$el.empty().fullCalendar({
|
this.$el.empty().fullCalendar({
|
||||||
defaultView : 'basicWeek',
|
defaultView : Config.getValue(this.storageKey, 'basicWeek'),
|
||||||
allDayDefault : false,
|
allDayDefault : false,
|
||||||
ignoreTimezone: false,
|
ignoreTimezone: false,
|
||||||
weekMode : 'variable',
|
weekMode : 'variable',
|
||||||
|
@ -41,16 +40,35 @@ define(
|
||||||
prev: '<i class="icon-arrow-left"></i>',
|
prev: '<i class="icon-arrow-left"></i>',
|
||||||
next: '<i class="icon-arrow-right"></i>'
|
next: '<i class="icon-arrow-right"></i>'
|
||||||
},
|
},
|
||||||
viewRender : this._getEvents,
|
viewRender : this._viewRender.bind(this),
|
||||||
eventRender : function (event, element) {
|
eventRender : this._eventRender.bind(this),
|
||||||
self.$(element).addClass(event.statusLevel);
|
eventClick : function (event) {
|
||||||
self.$(element).children('.fc-event-inner').addClass(event.statusLevel);
|
vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow: function () {
|
||||||
|
this.$('.fc-button-today').click();
|
||||||
|
},
|
||||||
|
|
||||||
|
_viewRender: function (view) {
|
||||||
|
if (Config.getValue(this.storageKey) !== view.name) {
|
||||||
|
Config.setValue(this.storageKey, view.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getEvents(view);
|
||||||
|
},
|
||||||
|
|
||||||
|
_eventRender: function (event, element) {
|
||||||
|
this.$(element).addClass(event.statusLevel);
|
||||||
|
this.$(element).children('.fc-event-inner').addClass(event.statusLevel);
|
||||||
|
|
||||||
if (event.progress > 0) {
|
if (event.progress > 0) {
|
||||||
self.$(element).find('.fc-event-time')
|
this.$(element).find('.fc-event-time')
|
||||||
.after('<span class="chart pull-right" data-percent="{0}"></span>'.format(event.progress));
|
.after('<span class="chart pull-right" data-percent="{0}"></span>'.format(event.progress));
|
||||||
|
|
||||||
self.$(element).find('.chart').easyPieChart({
|
this.$(element).find('.chart').easyPieChart({
|
||||||
barColor : '#ffffff',
|
barColor : '#ffffff',
|
||||||
trackColor: false,
|
trackColor: false,
|
||||||
scaleColor: false,
|
scaleColor: false,
|
||||||
|
@ -60,35 +78,24 @@ define(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
eventClick : function (event) {
|
|
||||||
vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_instance = this;
|
|
||||||
},
|
|
||||||
|
|
||||||
onShow: function () {
|
|
||||||
this.$('.fc-button-today').click();
|
|
||||||
},
|
|
||||||
|
|
||||||
_getEvents: function (view) {
|
_getEvents: function (view) {
|
||||||
var start = moment(view.visStart).toISOString();
|
var start = moment(view.visStart).toISOString();
|
||||||
var end = moment(view.visEnd).toISOString();
|
var end = moment(view.visEnd).toISOString();
|
||||||
|
|
||||||
_instance.$el.fullCalendar('removeEvents');
|
this.$el.fullCalendar('removeEvents');
|
||||||
|
|
||||||
_instance.collection.fetch({
|
this.collection.fetch({
|
||||||
data : { start: start, end: end },
|
data : { start: start, end: end },
|
||||||
success: function (collection) {
|
success: this._setEventData.bind(this)
|
||||||
_instance._setEventData(collection);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_setEventData: function (collection) {
|
_setEventData: function (collection) {
|
||||||
var events = [];
|
var events = [];
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
collection.each(function (model) {
|
collection.each(function (model) {
|
||||||
var seriesTitle = model.get('series').title;
|
var seriesTitle = model.get('series').title;
|
||||||
var start = model.get('airDateUtc');
|
var start = model.get('airDateUtc');
|
||||||
|
@ -100,15 +107,15 @@ define(
|
||||||
start : start,
|
start : start,
|
||||||
end : end,
|
end : end,
|
||||||
allDay : false,
|
allDay : false,
|
||||||
statusLevel : _instance._getStatusLevel(model, end),
|
statusLevel : self._getStatusLevel(model, end),
|
||||||
progress : _instance._getDownloadProgress(model),
|
progress : self._getDownloadProgress(model),
|
||||||
model : model
|
model : model
|
||||||
};
|
};
|
||||||
|
|
||||||
events.push(event);
|
events.push(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
_instance.$el.fullCalendar('addEventSource', events);
|
this.$el.fullCalendar('addEventSource', events);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getStatusLevel: function (element, endTime) {
|
_getStatusLevel: function (element, endTime) {
|
||||||
|
|
|
@ -158,3 +158,17 @@
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ical
|
||||||
|
{
|
||||||
|
color: @btnInverseBackground;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ical-url {
|
||||||
|
|
||||||
|
input {
|
||||||
|
width : 440px;
|
||||||
|
cursor : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ define(
|
||||||
mode: 'client',
|
mode: 'client',
|
||||||
|
|
||||||
findEpisode: function (episodeId) {
|
findEpisode: function (episodeId) {
|
||||||
return _.find(this.models, function (queueModel) {
|
return _.find(this.fullCollection.models, function (queueModel) {
|
||||||
return queueModel.get('episode').id === episodeId;
|
return queueModel.get('episode').id === episodeId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group advanced-setting">
|
<div class="control-group advanced-setting">
|
||||||
<label class="control-label">Drone Factory Internal</label>
|
<label class="control-label">Drone Factory Interval</label>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input type="number" name="downloadedEpisodesScanInterval"/>
|
<input type="number" name="downloadedEpisodesScanInterval"/>
|
||||||
|
|
|
@ -59,13 +59,6 @@ define(
|
||||||
vent.trigger(vent.Events.ServerUpdated);
|
vent.trigger(vent.Events.ServerUpdated);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Messenger.show({
|
|
||||||
id : messengerId,
|
|
||||||
type : 'success',
|
|
||||||
hideAfter : 5,
|
|
||||||
message : 'Connection to backend restored'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.signalRconnection.disconnected(function () {
|
this.signalRconnection.disconnected(function () {
|
||||||
|
|
|
@ -6,7 +6,7 @@ define(
|
||||||
], function (Backbone, StatusModel) {
|
], function (Backbone, StatusModel) {
|
||||||
return Backbone.Model.extend({
|
return Backbone.Model.extend({
|
||||||
url: function () {
|
url: function () {
|
||||||
return StatusModel.get('urlBase') + '/log/' + this.get('filename');
|
return StatusModel.get('urlBase') + '/logfile/' + this.get('filename');
|
||||||
},
|
},
|
||||||
|
|
||||||
parse: function (contents) {
|
parse: function (contents) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ define(
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
this.$el.empty();
|
this.$el.empty();
|
||||||
this.$el.html('<a href="{0}/log/{1}" class="no-router" target="_blank">Download</a>'.format(StatusModel.get('urlBase'), this.cellValue));
|
this.$el.html('<a href="{0}/logfile/{1}" class="no-router" target="_blank">Download</a>'.format(StatusModel.get('urlBase'), this.cellValue));
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ define(
|
||||||
'System/Logs/Files/LogFileModel'
|
'System/Logs/Files/LogFileModel'
|
||||||
], function (Backbone, LogFileModel) {
|
], function (Backbone, LogFileModel) {
|
||||||
return Backbone.Collection.extend({
|
return Backbone.Collection.extend({
|
||||||
url : window.NzbDrone.ApiRoot + '/log/files',
|
url : window.NzbDrone.ApiRoot + '/log/file',
|
||||||
model: LogFileModel,
|
model: LogFileModel,
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
|
|
|
@ -67,7 +67,7 @@ define(
|
||||||
name : 'this',
|
name : 'this',
|
||||||
label : 'Episode Title',
|
label : 'Episode Title',
|
||||||
sortable : false,
|
sortable : false,
|
||||||
cell : EpisodeTitleCell,
|
cell : EpisodeTitleCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'airDateUtc',
|
name : 'airDateUtc',
|
||||||
|
@ -121,10 +121,24 @@ define(
|
||||||
callback: this._searchSelected,
|
callback: this._searchSelected,
|
||||||
ownerContext: this
|
ownerContext: this
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Search All Missing',
|
||||||
|
icon : 'icon-search',
|
||||||
|
callback: this._searchMissing,
|
||||||
|
ownerContext: this
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Season Pass',
|
title: 'Season Pass',
|
||||||
icon : 'icon-bookmark',
|
icon : 'icon-bookmark',
|
||||||
route: 'seasonpass'
|
route: 'seasonpass'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rescan Drone Factory Folder',
|
||||||
|
icon : 'icon-refresh',
|
||||||
|
command: 'downloadedepisodesscan',
|
||||||
|
properties: {
|
||||||
|
sendUpdates: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -201,6 +215,16 @@ define(
|
||||||
name : 'episodeSearch',
|
name : 'episodeSearch',
|
||||||
episodeIds: ids
|
episodeIds: ids
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_searchMissing: function () {
|
||||||
|
if (window.confirm('Are you sure you want to search for {0} missing episodes? '.format(this.collection.state.totalRecords) +
|
||||||
|
'One API request to each indexer will be used for each episode. ' +
|
||||||
|
'This cannot be stopped once started.')) {
|
||||||
|
CommandController.Execute('missingEpisodeSearch', {
|
||||||
|
name : 'missingEpisodeSearch'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
<link rel="apple-touch-icon" sizes="114x114" href="/Content/Images/touch/114.png?v=2"/>
|
<link rel="apple-touch-icon" sizes="114x114" href="/Content/Images/touch/114.png?v=2"/>
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="/Content/Images/touch/144.png?v=2"/>
|
<link rel="apple-touch-icon" sizes="144x144" href="/Content/Images/touch/144.png?v=2"/>
|
||||||
<link rel="icon" type="image/ico" href="/Content/Images/favicon.ico?v=2"/>
|
<link rel="icon" type="image/ico" href="/Content/Images/favicon.ico?v=2"/>
|
||||||
|
|
||||||
|
<link rel="alternate" type="text/calendar" title="iCalendar feed for NzbDrone" href="/feed/calendar/NzbDrone.ics" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="nav-region"></div>
|
<div id="nav-region"></div>
|
||||||
|
|
|
@ -29,17 +29,21 @@ define(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var href = event.target.getAttribute('href');
|
var href = event.target.getAttribute('href');
|
||||||
|
|
||||||
if (!href && $target.closest('a') && $target.closest('a')[0]) {
|
if (!href && $target.closest('a') && $target.closest('a')[0]) {
|
||||||
|
|
||||||
var linkElement = $target.closest('a')[0];
|
var linkElement = $target.closest('a')[0];
|
||||||
|
|
||||||
|
if ($(linkElement).hasClass('no-router')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
href = linkElement.getAttribute('href');
|
href = linkElement.getAttribute('href');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
if (!href) {
|
if (!href) {
|
||||||
throw 'couldn\'t find route target';
|
throw 'couldn\'t find route target';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue