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="linux" 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>
|
||||
|
|
|
@ -95,6 +95,9 @@
|
|||
</None>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<!-- 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)
|
||||
{
|
||||
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,
|
||||
IDiskProvider diskProvider)
|
||||
: base("log/files")
|
||||
: base("log/file")
|
||||
{
|
||||
_appFolderInfo = appFolderInfo;
|
||||
_diskProvider = diskProvider;
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<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">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<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\BlacklistResource.cs" />
|
||||
<Compile Include="Calendar\CalendarModule.cs" />
|
||||
<Compile Include="Calendar\CalendarFeedModule.cs" />
|
||||
<Compile Include="ClientSchema\SchemaDeserializer.cs" />
|
||||
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
|
||||
<Compile Include="ClientSchema\Field.cs" />
|
||||
|
@ -139,6 +143,7 @@
|
|||
<Compile Include="Metadata\MetadataResource.cs" />
|
||||
<Compile Include="Metadata\MetadataModule.cs" />
|
||||
<Compile Include="Notifications\NotificationSchemaModule.cs" />
|
||||
<Compile Include="NzbDroneFeedModule.cs" />
|
||||
<Compile Include="ProviderResource.cs" />
|
||||
<Compile Include="ProviderModuleBase.cs" />
|
||||
<Compile Include="Indexers\IndexerSchemaModule.cs" />
|
||||
|
@ -199,7 +204,9 @@
|
|||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="packages.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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"?>
|
||||
<packages>
|
||||
<package id="DDay.iCal" version="1.0.2.575" targetFramework="net40" />
|
||||
<package id="FluentValidation" version="5.0.0.1" targetFramework="net40" />
|
||||
<package id="Nancy" version="0.21.1" targetFramework="net40" />
|
||||
<package id="Nancy.Authentication.Basic" version="0.21.1" targetFramework="net40" />
|
||||
|
|
|
@ -95,6 +95,9 @@
|
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -82,6 +82,9 @@
|
|||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.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.
|
||||
|
|
|
@ -111,6 +111,9 @@
|
|||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<!-- 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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileAttributes GetFileAttributes(string path)
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
@ -18,7 +19,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
|||
IsLinux = IsMono && !IsOsx;
|
||||
IsWindows = !IsMono;
|
||||
|
||||
FirstDayOfWeek = DateTime.Today.GetFirstDayOfWeek().DayOfWeek;
|
||||
FirstDayOfWeek = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
|
||||
|
||||
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\IContainer.cs" />
|
||||
<Compile Include="Composition\ContainerBuilderBase.cs" />
|
||||
<Compile Include="DateTimeExtensions.cs" />
|
||||
<Compile Include="DictionaryExtensions.cs" />
|
||||
<Compile Include="Disk\DiskProviderBase.cs" />
|
||||
<Compile Include="EnsureThat\Ensure.cs" />
|
||||
|
@ -105,8 +104,10 @@
|
|||
<Compile Include="PathEqualityComparer.cs" />
|
||||
<Compile Include="Processes\INzbDroneProcessProvider.cs" />
|
||||
<Compile Include="Processes\ProcessOutput.cs" />
|
||||
<Compile Include="RateGate.cs" />
|
||||
<Compile Include="Serializer\IntConverter.cs" />
|
||||
<Compile Include="Services.cs" />
|
||||
<Compile Include="Extensions\StreamExtensions.cs" />
|
||||
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
|
||||
<Compile Include="Security\IgnoreCertErrorPolicy.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.IO;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.Nzbget;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
@ -46,16 +48,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
|
|||
[Test]
|
||||
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>()
|
||||
.Setup(s => s.AddNzb(It.IsAny<NzbgetSettings>(), p))
|
||||
.Returns(true);
|
||||
.Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()))
|
||||
.Returns("id");
|
||||
|
||||
Subject.DownloadNzb(_remoteEpisode);
|
||||
|
||||
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)
|
||||
.All()
|
||||
.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()
|
||||
.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.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Practices.ObjectBuilder2;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Jobs;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
|
|
|
@ -136,6 +136,7 @@
|
|||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecsFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFilesFixture.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
|
||||
<Compile Include="IndexerSearchTests\SearchDefinitionFixture.cs" />
|
||||
<Compile Include="IndexerTests\BasicRssParserFixture.cs" />
|
||||
|
@ -231,6 +232,7 @@
|
|||
<Compile Include="TvTests\SeriesRepositoryTests\QualityProfileRepositoryFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\UpdateMultipleSeriesFixture.cs" />
|
||||
<Compile Include="TvTests\SeriesServiceTests\UpdateSeriesFixture.cs" />
|
||||
<Compile Include="TvTests\ShouldRefreshSeriesFixture.cs" />
|
||||
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
||||
<Compile Include="Qualities\QualityDefinitionServiceFixture.cs" />
|
||||
|
@ -374,6 +376,9 @@
|
|||
<ItemGroup>
|
||||
<Folder Include="ProviderTests\UpdateProviderTests\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PreBuildEvent>
|
||||
|
|
|
@ -27,6 +27,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||
[TestCase("86420f8ee425340d8894bf3bc636b66404b95f18")]
|
||||
[TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")]
|
||||
[TestCase("THIS SHOULD NEVER PARSE")]
|
||||
[TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")]
|
||||
public void should_not_parse_crap(string title)
|
||||
{
|
||||
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\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\The.Blacklist.S01E16.720p.HDTV.X264-DIMENSION\XRmZciqkBopq4851Ddbipe\Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv", 1, 16)]
|
||||
public void should_parse_from_path(string path, int season, int episode)
|
||||
{
|
||||
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("Eureka S 01 720p WEB DL DD 5 1 h264 TjHD", "Eureka", 1)]
|
||||
[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)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
|
|
|
@ -4,10 +4,10 @@ using System.IO;
|
|||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.RootFolderTests
|
||||
|
@ -28,7 +28,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
|||
.Returns(new List<RootFolder>());
|
||||
}
|
||||
|
||||
private void WithNoneExistingFolder()
|
||||
private void WithNonExistingFolder()
|
||||
{
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(m => m.FolderExists(It.IsAny<string>()))
|
||||
|
@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
|||
[Test]
|
||||
public void should_throw_if_folder_being_added_doesnt_exist()
|
||||
{
|
||||
WithNoneExistingFolder();
|
||||
WithNonExistingFolder();
|
||||
|
||||
Assert.Throws<DirectoryNotFoundException>(() => Subject.Add(new RootFolder { Path = "C:\\TEST".AsOsAgnostic() }));
|
||||
}
|
||||
|
@ -62,9 +62,9 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
|||
}
|
||||
|
||||
[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>());
|
||||
|
||||
|
@ -100,5 +100,26 @@ namespace NzbDrone.Core.Test.RootFolderTests
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly IMigrationController _migrationController;
|
||||
|
@ -79,8 +78,11 @@ namespace NzbDrone.Core.Datastore
|
|||
return dataMapper;
|
||||
});
|
||||
|
||||
db.Vacuum();
|
||||
|
||||
if (migrationType == MigrationType.Main)
|
||||
{
|
||||
db.Vacuum();
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
|
|
@ -97,6 +97,11 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
|
||||
if (decision != null)
|
||||
{
|
||||
if (decision.Rejections.Any())
|
||||
{
|
||||
_logger.Debug("Release rejected for the following reasons: {0}", String.Join(", ", decision.Rejections));
|
||||
}
|
||||
|
||||
yield return decision;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
{
|
||||
|
@ -6,10 +7,13 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
{
|
||||
private string _nzbName;
|
||||
public Int32 NzbId { get; set; }
|
||||
public Int32 FirstId { get; set; }
|
||||
public Int32 LastId { get; set; }
|
||||
public string NzbName { get; set; }
|
||||
public String Category { get; set; }
|
||||
public Int32 FileSizeMb { get; set; }
|
||||
public Int32 RemainingSizeMb { get; set; }
|
||||
public Int32 PausedSizeMb { get; set; }
|
||||
public List<NzbgetParameter> Parameters { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
@ -13,14 +14,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
{
|
||||
private readonly INzbgetProxy _proxy;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IHttpProvider _httpProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public Nzbget(INzbgetProxy proxy,
|
||||
IParsingService parsingService,
|
||||
IHttpProvider httpProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_parsingService = parsingService;
|
||||
_httpProvider = httpProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -29,16 +33,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
var url = remoteEpisode.Release.DownloadUrl;
|
||||
var title = remoteEpisode.Release.Title + ".nzb";
|
||||
|
||||
string cat = Settings.TvCategory;
|
||||
string category = Settings.TvCategory;
|
||||
int priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
|
||||
|
||||
_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 null;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<QueueItem> GetQueue()
|
||||
|
@ -57,14 +63,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
|
||||
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();
|
||||
queueItem.Id = nzbGetQueueItem.NzbId.ToString();
|
||||
queueItem.Title = nzbGetQueueItem.NzbName;
|
||||
queueItem.Size = nzbGetQueueItem.FileSizeMb;
|
||||
queueItem.Sizeleft = nzbGetQueueItem.RemainingSizeMb;
|
||||
queueItem.Status = nzbGetQueueItem.FileSizeMb == nzbGetQueueItem.PausedSizeMb ? "paused" : "queued";
|
||||
queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
|
||||
queueItem.Title = item.NzbName;
|
||||
queueItem.Size = item.FileSizeMb;
|
||||
queueItem.Sizeleft = item.RemainingSizeMb;
|
||||
queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued";
|
||||
|
||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
@ -91,7 +135,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
|
||||
public override void RemoveFromHistory(string id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
_proxy.RemoveFromHistory(id, Settings);
|
||||
}
|
||||
|
||||
public override void Test()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
{
|
||||
public class EnqueueResponse
|
||||
public class NzbgetBooleanResponse
|
||||
{
|
||||
public String Version { 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
|
||||
{
|
||||
public class NzbgetQueue
|
||||
public class NzbgetListResponse<T>
|
||||
{
|
||||
public String Version { get; set; }
|
||||
|
||||
[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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Rest;
|
||||
using RestSharp;
|
||||
|
@ -9,9 +12,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
{
|
||||
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<NzbgetHistoryItem> GetHistory(NzbgetSettings settings);
|
||||
VersionResponse GetVersion(NzbgetSettings settings);
|
||||
void RemoveFromHistory(string id, NzbgetSettings settings);
|
||||
}
|
||||
|
||||
public class NzbgetProxy : INzbgetProxy
|
||||
|
@ -23,18 +28,50 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
_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)
|
||||
{
|
||||
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)
|
||||
|
@ -44,6 +81,32 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
|||
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)
|
||||
{
|
||||
var client = BuildClient(settings);
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
|
||||
using NzbDrone.Core.Instrumentation.Extensions;
|
||||
|
@ -35,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||
var request = new RestRequest(Method.POST);
|
||||
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;
|
||||
|
||||
|
@ -161,20 +162,5 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
|||
if (result.Failed)
|
||||
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.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
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
|
||||
{
|
||||
public class EpisodeSearchCommand : Command
|
||||
public class MissingEpisodeSearchCommand : Command
|
||||
{
|
||||
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.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class EpisodeSearchService : IExecute<EpisodeSearchCommand>
|
||||
public class MissingEpisodeSearchService : IExecute<EpisodeSearchCommand>, IExecute<MissingEpisodeSearchCommand>
|
||||
{
|
||||
private readonly ISearchForNzb _nzbSearchService;
|
||||
private readonly IDownloadApprovedReports _downloadApprovedReports;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public EpisodeSearchService(ISearchForNzb nzbSearchService,
|
||||
public MissingEpisodeSearchService(ISearchForNzb nzbSearchService,
|
||||
IDownloadApprovedReports downloadApprovedReports,
|
||||
IEpisodeService episodeService,
|
||||
Logger logger)
|
||||
{
|
||||
_nzbSearchService = nzbSearchService;
|
||||
_downloadApprovedReports = downloadApprovedReports;
|
||||
_episodeService = episodeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -30,5 +38,39 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
_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
|
||||
{
|
||||
List<DownloadDecision> EpisodeSearch(int episodeId);
|
||||
List<DownloadDecision> EpisodeSearch(Episode episode);
|
||||
List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber);
|
||||
}
|
||||
|
||||
|
@ -52,6 +53,12 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
public List<DownloadDecision> EpisodeSearch(int episodeId)
|
||||
{
|
||||
var episode = _episodeService.GetEpisode(episodeId);
|
||||
|
||||
return EpisodeSearch(episode);
|
||||
}
|
||||
|
||||
public List<DownloadDecision> EpisodeSearch(Episode episode)
|
||||
{
|
||||
var series = _seriesService.GetSeries(episode.SeriesId);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (errorMessage == "Request limit reached")
|
||||
{
|
||||
throw new RequestLimitReachedException("API limit reached");
|
||||
}
|
||||
|
||||
throw new NewznabException("Newznab error detected: {0}", errorMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,10 @@ namespace NzbDrone.Core.Indexers
|
|||
var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key");
|
||||
throw new ValidationException(new List<ValidationFailure> {apiKeyFailure}.ToArray());
|
||||
}
|
||||
catch (RequestLimitReachedException)
|
||||
{
|
||||
_logger.Warn("Request limit reached");
|
||||
}
|
||||
catch (Exception 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[]
|
||||
{
|
||||
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
|
||||
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
|
||||
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
|
||||
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
|
||||
|
|
|
@ -4,6 +4,14 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
|||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
|
@ -16,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
{
|
||||
public interface IDiskScanService
|
||||
{
|
||||
void Scan(Series series);
|
||||
string[] GetVideoFiles(string path, bool allDirectories = true);
|
||||
}
|
||||
|
||||
|
@ -52,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
private void Scan(Series series)
|
||||
public void Scan(Series series)
|
||||
{
|
||||
_logger.ProgressInfo("Scanning disk for {0}", series.Title);
|
||||
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
|
||||
|
@ -73,9 +75,16 @@ namespace NzbDrone.Core.MediaFiles
|
|||
return;
|
||||
}
|
||||
|
||||
var videoFilesStopwatch = Stopwatch.StartNew();
|
||||
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);
|
||||
decisionsStopwatch.Stop();
|
||||
_logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
|
||||
|
||||
_importApprovedEpisodes.Import(decisions);
|
||||
|
||||
_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.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Remoting.Messaging;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
|
@ -10,7 +9,6 @@ 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;
|
||||
|
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||
using NLog;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Metadata.Consumers.Fake;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Metadata
|
||||
|
@ -30,8 +29,6 @@ namespace NzbDrone.Core.Metadata
|
|||
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
if (provider.GetType() == typeof(FakeMetadata)) continue;;
|
||||
|
||||
definitions.Add(new MetadataDefinition
|
||||
{
|
||||
Enable = false,
|
||||
|
|
|
@ -236,6 +236,8 @@
|
|||
<Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" />
|
||||
<Compile Include="Download\Clients\DownloadClientException.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\TestNzbgetCommand.cs" />
|
||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||
|
@ -280,12 +282,14 @@
|
|||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
||||
<Compile Include="Housekeeping\IHousekeepingTask.cs" />
|
||||
<Compile Include="IndexerSearch\MissingEpisodeSearchCommand.cs" />
|
||||
<Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
|
||||
<Compile Include="IndexerSearch\SeriesSearchService.cs" />
|
||||
<Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
|
||||
|
@ -296,6 +300,7 @@
|
|||
<Compile Include="Indexers\BasicTorrentRssParser.cs" />
|
||||
<Compile Include="Indexers\DownloadProtocols.cs" />
|
||||
<Compile Include="Indexers\Exceptions\ApiKeyException.cs" />
|
||||
<Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" />
|
||||
<Compile Include="Indexers\Eztv\Eztv.cs" />
|
||||
<Compile Include="Indexers\FetchAndParseRssService.cs" />
|
||||
<Compile Include="Indexers\IIndexer.cs" />
|
||||
|
@ -341,9 +346,11 @@
|
|||
<Compile Include="MetadataSource\Trakt\Actor.cs" />
|
||||
<Compile Include="MetadataSource\Trakt\People.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\Consumers\Fake\Fake.cs" />
|
||||
<Compile Include="Metadata\Consumers\Fake\FakeSettings.cs" />
|
||||
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
|
||||
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
|
||||
<Compile Include="Metadata\ExistingMetadataService.cs" />
|
||||
|
@ -498,10 +505,10 @@
|
|||
<Compile Include="Instrumentation\LogService.cs" />
|
||||
<Compile Include="Instrumentation\DatabaseTarget.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\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\NzbgetPriority.cs" />
|
||||
<Compile Include="Download\Clients\Nzbget\VersionResponse.cs" />
|
||||
|
@ -663,6 +670,7 @@
|
|||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Tv\SeriesStatusType.cs" />
|
||||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||
<Compile Include="Tv\ShouldRefreshSeries.cs" />
|
||||
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
|
||||
<Compile Include="Update\Commands\InstallUpdateCommand.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|_",
|
||||
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);
|
||||
|
||||
private static readonly Regex MultiPartCleanupRegex = new Regex(@"\(\d+\)$", RegexOptions.Compiled);
|
||||
|
|
|
@ -31,7 +31,17 @@ namespace NzbDrone.Core.RootFolders
|
|||
private readonly IConfigService _configService;
|
||||
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,
|
||||
|
@ -123,11 +133,8 @@ namespace NzbDrone.Core.RootFolders
|
|||
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
|
||||
}
|
||||
|
||||
if (Path.GetPathRoot(path).Equals(path, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var setToRemove = SpecialFolders;
|
||||
results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name));
|
||||
}
|
||||
|
||||
Logger.Debug("{0} unmapped folders detected.", results.Count);
|
||||
return results;
|
||||
|
|
|
@ -5,6 +5,8 @@ using System.Linq;
|
|||
using NLog;
|
||||
using NzbDrone.Core.DataAugmentation.DailySeries;
|
||||
using NzbDrone.Core.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
|
@ -21,15 +23,26 @@ namespace NzbDrone.Core.Tv
|
|||
private readonly IRefreshEpisodeService _refreshEpisodeService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IDailySeriesService _dailySeriesService;
|
||||
private readonly IDiskScanService _diskScanService;
|
||||
private readonly ICheckIfSeriesShouldBeRefreshed _checkIfSeriesShouldBeRefreshed;
|
||||
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;
|
||||
_seriesService = seriesService;
|
||||
_refreshEpisodeService = refreshEpisodeService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_dailySeriesService = dailySeriesService;
|
||||
_diskScanService = diskScanService;
|
||||
_checkIfSeriesShouldBeRefreshed = checkIfSeriesShouldBeRefreshed;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -115,6 +128,8 @@ namespace NzbDrone.Core.Tv
|
|||
var allSeries = _seriesService.GetAllSeries().OrderBy(c => c.Title).ToList();
|
||||
|
||||
foreach (var series in allSeries)
|
||||
{
|
||||
if (_checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -125,6 +140,20 @@ namespace NzbDrone.Core.Tv
|
|||
_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 wildcardHttpsUrls = BuildUrls("https", "*", _configFileProvider.SslPort);
|
||||
|
||||
if (!_configFileProvider.EnableSsl)
|
||||
{
|
||||
localHttpsUrls.Clear();
|
||||
wildcardHttpsUrls.Clear();
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
|
||||
{
|
||||
var httpUrls = wildcardHttpUrls.All(IsRegistered) ? wildcardHttpUrls : localHttpUrls;
|
||||
|
@ -51,7 +57,6 @@ namespace NzbDrone.Host.AccessControl
|
|||
Urls.AddRange(httpUrls);
|
||||
Urls.AddRange(httpsUrls);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Urls.AddRange(wildcardHttpUrls);
|
||||
|
|
|
@ -148,6 +148,9 @@
|
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -76,7 +76,9 @@
|
|||
<Name>NzbDrone.Test.Common</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<!-- 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>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.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.
|
||||
|
|
|
@ -87,6 +87,9 @@
|
|||
<Name>NzbDrone.Update</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
|
|
@ -101,6 +101,9 @@
|
|||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.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.
|
||||
|
|
|
@ -8,16 +8,16 @@
|
|||
<option es3="false" />
|
||||
<option forin="true" />
|
||||
<option immed="true" />
|
||||
<option latedef="true" />
|
||||
<option newcap="true" />
|
||||
<option noarg="true" />
|
||||
<option noempty="false" />
|
||||
<option nonew="true" />
|
||||
<option plusplus="false" />
|
||||
<option undef="true" />
|
||||
<option unused="true" />
|
||||
<option strict="true" />
|
||||
<option trailing="false" />
|
||||
<option latedef="true" />
|
||||
<option unused="true" />
|
||||
<option quotmark="single" />
|
||||
<option maxdepth="3" />
|
||||
<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';
|
||||
define(
|
||||
[
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Calendar/UpcomingCollectionView',
|
||||
'Calendar/CalendarView'
|
||||
], function (Marionette, UpcomingCollectionView, CalendarView) {
|
||||
'Calendar/CalendarView',
|
||||
'Calendar/CalendarFeedView'
|
||||
], function (AppLayout, Marionette, UpcomingCollectionView, CalendarView, CalendarFeedView) {
|
||||
return Marionette.Layout.extend({
|
||||
template: 'Calendar/CalendarLayoutTemplate',
|
||||
|
||||
|
@ -13,6 +15,10 @@ define(
|
|||
calendar: '#x-calendar'
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .x-ical': '_showiCal'
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this._showUpcoming();
|
||||
this._showCalendar();
|
||||
|
@ -24,6 +30,11 @@ define(
|
|||
|
||||
_showCalendar: function () {
|
||||
this.calendar.show(new CalendarView());
|
||||
},
|
||||
|
||||
_showiCal: function () {
|
||||
var view = new CalendarFeedView();
|
||||
AppLayout.modalRegion.show(view);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
<div class="row">
|
||||
<div class="span3">
|
||||
<div class="pull-left">
|
||||
<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>
|
||||
<div class=span9>
|
||||
|
|
|
@ -8,25 +8,24 @@ define(
|
|||
'Calendar/Collection',
|
||||
'System/StatusModel',
|
||||
'History/Queue/QueueCollection',
|
||||
'Config',
|
||||
'Mixins/backbone.signalr.mixin',
|
||||
'fullcalendar',
|
||||
'jquery.easypiechart'
|
||||
], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) {
|
||||
|
||||
var _instance;
|
||||
], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection, Config) {
|
||||
|
||||
return Marionette.ItemView.extend({
|
||||
storageKey: 'calendar.view',
|
||||
|
||||
initialize: function () {
|
||||
this.collection = new CalendarCollection().bindSignalR({ updateOnly: true });
|
||||
this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
|
||||
this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents);
|
||||
},
|
||||
|
||||
render : function () {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.$el.empty().fullCalendar({
|
||||
defaultView : 'basicWeek',
|
||||
defaultView : Config.getValue(this.storageKey, 'basicWeek'),
|
||||
allDayDefault : false,
|
||||
ignoreTimezone: false,
|
||||
weekMode : 'variable',
|
||||
|
@ -41,16 +40,35 @@ define(
|
|||
prev: '<i class="icon-arrow-left"></i>',
|
||||
next: '<i class="icon-arrow-right"></i>'
|
||||
},
|
||||
viewRender : this._getEvents,
|
||||
eventRender : function (event, element) {
|
||||
self.$(element).addClass(event.statusLevel);
|
||||
self.$(element).children('.fc-event-inner').addClass(event.statusLevel);
|
||||
viewRender : this._viewRender.bind(this),
|
||||
eventRender : this._eventRender.bind(this),
|
||||
eventClick : function (event) {
|
||||
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) {
|
||||
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));
|
||||
|
||||
self.$(element).find('.chart').easyPieChart({
|
||||
this.$(element).find('.chart').easyPieChart({
|
||||
barColor : '#ffffff',
|
||||
trackColor: 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) {
|
||||
var start = moment(view.visStart).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 },
|
||||
success: function (collection) {
|
||||
_instance._setEventData(collection);
|
||||
}
|
||||
success: this._setEventData.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
_setEventData: function (collection) {
|
||||
var events = [];
|
||||
|
||||
var self = this;
|
||||
|
||||
collection.each(function (model) {
|
||||
var seriesTitle = model.get('series').title;
|
||||
var start = model.get('airDateUtc');
|
||||
|
@ -100,15 +107,15 @@ define(
|
|||
start : start,
|
||||
end : end,
|
||||
allDay : false,
|
||||
statusLevel : _instance._getStatusLevel(model, end),
|
||||
progress : _instance._getDownloadProgress(model),
|
||||
statusLevel : self._getStatusLevel(model, end),
|
||||
progress : self._getDownloadProgress(model),
|
||||
model : model
|
||||
};
|
||||
|
||||
events.push(event);
|
||||
});
|
||||
|
||||
_instance.$el.fullCalendar('addEventSource', events);
|
||||
this.$el.fullCalendar('addEventSource', events);
|
||||
},
|
||||
|
||||
_getStatusLevel: function (element, endTime) {
|
||||
|
|
|
@ -158,3 +158,17 @@
|
|||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ical
|
||||
{
|
||||
color: @btnInverseBackground;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ical-url {
|
||||
|
||||
input {
|
||||
width : 440px;
|
||||
cursor : text;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ define(
|
|||
mode: 'client',
|
||||
|
||||
findEpisode: function (episodeId) {
|
||||
return _.find(this.models, function (queueModel) {
|
||||
return _.find(this.fullCollection.models, function (queueModel) {
|
||||
return queueModel.get('episode').id === episodeId;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<input type="number" name="downloadedEpisodesScanInterval"/>
|
||||
|
|
|
@ -59,13 +59,6 @@ define(
|
|||
vent.trigger(vent.Events.ServerUpdated);
|
||||
}
|
||||
});
|
||||
|
||||
Messenger.show({
|
||||
id : messengerId,
|
||||
type : 'success',
|
||||
hideAfter : 5,
|
||||
message : 'Connection to backend restored'
|
||||
});
|
||||
});
|
||||
|
||||
this.signalRconnection.disconnected(function () {
|
||||
|
|
|
@ -6,7 +6,7 @@ define(
|
|||
], function (Backbone, StatusModel) {
|
||||
return Backbone.Model.extend({
|
||||
url: function () {
|
||||
return StatusModel.get('urlBase') + '/log/' + this.get('filename');
|
||||
return StatusModel.get('urlBase') + '/logfile/' + this.get('filename');
|
||||
},
|
||||
|
||||
parse: function (contents) {
|
||||
|
|
|
@ -10,7 +10,7 @@ define(
|
|||
|
||||
render: function () {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ define(
|
|||
'System/Logs/Files/LogFileModel'
|
||||
], function (Backbone, LogFileModel) {
|
||||
return Backbone.Collection.extend({
|
||||
url : window.NzbDrone.ApiRoot + '/log/files',
|
||||
url : window.NzbDrone.ApiRoot + '/log/file',
|
||||
model: LogFileModel,
|
||||
|
||||
state: {
|
||||
|
|
|
@ -67,7 +67,7 @@ define(
|
|||
name : 'this',
|
||||
label : 'Episode Title',
|
||||
sortable : false,
|
||||
cell : EpisodeTitleCell,
|
||||
cell : EpisodeTitleCell
|
||||
},
|
||||
{
|
||||
name : 'airDateUtc',
|
||||
|
@ -121,10 +121,24 @@ define(
|
|||
callback: this._searchSelected,
|
||||
ownerContext: this
|
||||
},
|
||||
{
|
||||
title: 'Search All Missing',
|
||||
icon : 'icon-search',
|
||||
callback: this._searchMissing,
|
||||
ownerContext: this
|
||||
},
|
||||
{
|
||||
title: 'Season Pass',
|
||||
icon : 'icon-bookmark',
|
||||
route: 'seasonpass'
|
||||
},
|
||||
{
|
||||
title: 'Rescan Drone Factory Folder',
|
||||
icon : 'icon-refresh',
|
||||
command: 'downloadedepisodesscan',
|
||||
properties: {
|
||||
sendUpdates: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -201,6 +215,16 @@ define(
|
|||
name : 'episodeSearch',
|
||||
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="144x144" href="/Content/Images/touch/144.png?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>
|
||||
<body>
|
||||
<div id="nav-region"></div>
|
||||
|
|
|
@ -29,17 +29,21 @@ define(
|
|||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var href = event.target.getAttribute('href');
|
||||
|
||||
if (!href && $target.closest('a') && $target.closest('a')[0]) {
|
||||
|
||||
var linkElement = $target.closest('a')[0];
|
||||
|
||||
if ($(linkElement).hasClass('no-router')) {
|
||||
return;
|
||||
}
|
||||
|
||||
href = linkElement.getAttribute('href');
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (!href) {
|
||||
throw 'couldn\'t find route target';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue